mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-02-15 10:30:08 +00:00
Compare commits
3 Commits
feature/mu
...
refresh_wo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd8b92df9c | ||
|
|
14f7b51160 | ||
|
|
caf476e777 |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -7,7 +7,7 @@
|
||||
Please include a summary of the change and which issue is fixed. Also include relevant motivation and context. List any dependencies that are required for this change.
|
||||
|
||||
## Contribution Guidelines
|
||||
Please Read through the [Contribution Guidelines](https://github.com/aquasecurity/kube-hunter/blob/main/CONTRIBUTING.md).
|
||||
Please Read through the [Contribution Guidelines](https://github.com/aquasecurity/kube-hunter/blob/master/CONTRIBUTING.md).
|
||||
|
||||
## Fixed Issues
|
||||
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Lint
|
||||
|
||||
on: [push, pull_request]
|
||||
@@ -11,4 +10,3 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: pre-commit/action@v2.0.0
|
||||
- uses: ibiqlik/action-yamllint@v3
|
||||
|
||||
65
.github/workflows/publish.yml
vendored
65
.github/workflows/publish.yml
vendored
@@ -1,65 +0,0 @@
|
||||
---
|
||||
name: Publish
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
env:
|
||||
ALIAS: aquasecurity
|
||||
REP: kube-hunter
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildxarch-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildxarch-
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to ECR
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: public.ecr.aws
|
||||
username: ${{ secrets.ECR_ACCESS_KEY_ID }}
|
||||
password: ${{ secrets.ECR_SECRET_ACCESS_KEY }}
|
||||
- name: Get version
|
||||
id: get_version
|
||||
uses: crazy-max/ghaction-docker-meta@v1
|
||||
with:
|
||||
images: ${{ env.REP }}
|
||||
tag-semver: |
|
||||
{{version}}
|
||||
|
||||
- name: Build and push - Docker/ECR
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKERHUB_USER }}/${{ env.REP }}:${{ steps.get_version.outputs.version }}
|
||||
public.ecr.aws/${{ env.ALIAS }}/${{ env.REP }}:${{ steps.get_version.outputs.version }}
|
||||
${{ secrets.DOCKERHUB_USER }}/${{ env.REP }}:latest
|
||||
public.ecr.aws/${{ env.ALIAS }}/${{ env.REP }}:latest
|
||||
cache-from: type=local,src=/tmp/.buildx-cache/release
|
||||
cache-to: type=local,mode=max,dest=/tmp/.buildx-cache/release
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
53
.github/workflows/release.yml
vendored
53
.github/workflows/release.yml
vendored
@@ -1,53 +0,0 @@
|
||||
---
|
||||
on:
|
||||
push:
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
name: Release
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Upload Release Asset
|
||||
runs-on: ubuntu-16.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install -U pip
|
||||
python -m pip install -r requirements-dev.txt
|
||||
|
||||
- name: Build project
|
||||
shell: bash
|
||||
run: |
|
||||
make pyinstaller
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dist/kube-hunter
|
||||
asset_name: kube-hunter-linux-x86_64-${{ github.ref }}
|
||||
asset_content_type: application/octet-stream
|
||||
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
@@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
---
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: stable
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.7.9
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-bugbear]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: stable
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.7.9
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-bugbear]
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
extends: default
|
||||
|
||||
rules:
|
||||
line-length: disable
|
||||
truthy: disable
|
||||
28
README.md
28
README.md
@@ -1,18 +1,12 @@
|
||||

|
||||

|
||||
|
||||
[![GitHub Release][release-img]][release]
|
||||
![Downloads][download]
|
||||
![Docker Pulls][docker-pull]
|
||||
[](https://github.com/aquasecurity/kube-hunter/actions)
|
||||
[](https://codecov.io/gh/aquasecurity/kube-hunter)
|
||||
[](https://codecov.io/gh/aquasecurity/kube-hunter)
|
||||
[](https://github.com/psf/black)
|
||||
[](https://github.com/aquasecurity/kube-hunter/blob/main/LICENSE)
|
||||
[](https://github.com/aquasecurity/kube-hunter/blob/master/LICENSE)
|
||||
[](https://microbadger.com/images/aquasec/kube-hunter "Get your own image badge on microbadger.com")
|
||||
|
||||
[download]: https://img.shields.io/github/downloads/aquasecurity/kube-hunter/total?logo=github
|
||||
[release-img]: https://img.shields.io/github/release/aquasecurity/kube-hunter.svg?logo=github
|
||||
[release]: https://github.com/aquasecurity/kube-hunter/releases
|
||||
[docker-pull]: https://img.shields.io/docker/pulls/aquasec/kube-hunter?logo=docker&label=docker%20pulls%20%2F%20kube-hunter
|
||||
|
||||
|
||||
kube-hunter hunts for security weaknesses in Kubernetes clusters. The tool was developed to increase awareness and visibility for security issues in Kubernetes environments. **You should NOT run kube-hunter on a Kubernetes cluster that you don't own!**
|
||||
|
||||
@@ -20,9 +14,9 @@ kube-hunter hunts for security weaknesses in Kubernetes clusters. The tool was d
|
||||
|
||||
**Explore vulnerabilities**: The kube-hunter knowledge base includes articles about discoverable vulnerabilities and issues. When kube-hunter reports an issue, it will show its VID (Vulnerability ID) so you can look it up in the KB at https://aquasecurity.github.io/kube-hunter/
|
||||
|
||||
**Contribute**: We welcome contributions, especially new hunter modules that perform additional tests. If you would like to develop your modules please read [Guidelines For Developing Your First kube-hunter Module](https://github.com/aquasecurity/kube-hunter/blob/main/CONTRIBUTING.md).
|
||||
**Contribute**: We welcome contributions, especially new hunter modules that perform additional tests. If you would like to develop your modules please read [Guidelines For Developing Your First kube-hunter Module](https://github.com/aquasecurity/kube-hunter/blob/master/CONTRIBUTING.md).
|
||||
|
||||
[](https://youtu.be/s2-6rTkH8a8?t=57s)
|
||||
[](https://youtu.be/s2-6rTkH8a8?t=57s)
|
||||
|
||||
Table of Contents
|
||||
=================
|
||||
@@ -35,7 +29,6 @@ Table of Contents
|
||||
* [Nodes Mapping](#nodes-mapping)
|
||||
* [Output](#output)
|
||||
* [Dispatching](#dispatching)
|
||||
* [Advanced Usage](#advanced-usage)
|
||||
* [Deployment](#deployment)
|
||||
* [On Machine](#on-machine)
|
||||
* [Prerequisites](#prerequisites)
|
||||
@@ -115,11 +108,6 @@ Available dispatch methods are:
|
||||
* KUBEHUNTER_HTTP_DISPATCH_URL (defaults to: https://localhost)
|
||||
* KUBEHUNTER_HTTP_DISPATCH_METHOD (defaults to: POST)
|
||||
|
||||
### Advanced Usage
|
||||
#### Azure Quick Scanning
|
||||
When running **as a Pod in an Azure environment**, kube-hunter will fetch subnets from the Instance Metadata Service. Naturally this makes the discovery process take longer.
|
||||
To hardlimit subnet scanning to a `/24` CIDR, use the `--quick` option.
|
||||
|
||||
## Deployment
|
||||
There are three methods for deploying kube-hunter:
|
||||
|
||||
@@ -188,7 +176,7 @@ The example `job.yaml` file defines a Job that will run kube-hunter in a pod, us
|
||||
* View the test results with `kubectl logs <pod name>`
|
||||
|
||||
## Contribution
|
||||
To read the contribution guidelines, <a href="https://github.com/aquasecurity/kube-hunter/blob/main/CONTRIBUTING.md"> Click here </a>
|
||||
To read the contribution guidelines, <a href="https://github.com/aquasecurity/kube-hunter/blob/master/CONTRIBUTING.md"> Click here </a>
|
||||
|
||||
## License
|
||||
This repository is available under the [Apache License 2.0](https://github.com/aquasecurity/kube-hunter/blob/main/LICENSE).
|
||||
This repository is available under the [Apache License 2.0](https://github.com/aquasecurity/kube-hunter/blob/master/LICENSE).
|
||||
|
||||
17
SECURITY.md
17
SECURITY.md
@@ -1,17 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | ------------------ |
|
||||
| 0.4.x | :white_check_mark: |
|
||||
| 0.3.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
We encourage you to find vulnerabilities in kube-hunter.
|
||||
The process is simple, just report a Bug issue. and we will take a look at this.
|
||||
If you prefer to disclose privately, you can write to one of the security maintainers at:
|
||||
|
||||
| Name | Email |
|
||||
| ----------- | ------------------ |
|
||||
| Daniel Sagi | daniel.sagi@aquasec.com |
|
||||
@@ -1,12 +1,11 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activesupport (6.0.3.4)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
activesupport (4.2.11.1)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
zeitwerk (~> 2.2, >= 2.2.2)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
coffee-script (2.4.1)
|
||||
@@ -16,67 +15,65 @@ GEM
|
||||
colorator (1.1.0)
|
||||
commonmarker (0.17.13)
|
||||
ruby-enum (~> 0.5)
|
||||
concurrent-ruby (1.1.7)
|
||||
dnsruby (1.61.5)
|
||||
simpleidn (~> 0.1)
|
||||
em-websocket (0.5.2)
|
||||
concurrent-ruby (1.1.5)
|
||||
dnsruby (1.61.3)
|
||||
addressable (~> 2.5)
|
||||
em-websocket (0.5.1)
|
||||
eventmachine (>= 0.12.9)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
ethon (0.12.0)
|
||||
ffi (>= 1.3.0)
|
||||
eventmachine (1.2.7)
|
||||
execjs (2.7.0)
|
||||
faraday (1.3.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday (0.17.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ruby2_keywords
|
||||
faraday-net_http (1.0.1)
|
||||
ffi (1.14.2)
|
||||
ffi (1.11.1)
|
||||
forwardable-extended (2.6.0)
|
||||
gemoji (3.0.1)
|
||||
github-pages (209)
|
||||
github-pages (201)
|
||||
activesupport (= 4.2.11.1)
|
||||
github-pages-health-check (= 1.16.1)
|
||||
jekyll (= 3.9.0)
|
||||
jekyll-avatar (= 0.7.0)
|
||||
jekyll (= 3.8.5)
|
||||
jekyll-avatar (= 0.6.0)
|
||||
jekyll-coffeescript (= 1.1.1)
|
||||
jekyll-commonmark-ghpages (= 0.1.6)
|
||||
jekyll-default-layout (= 0.1.4)
|
||||
jekyll-feed (= 0.15.1)
|
||||
jekyll-feed (= 0.11.0)
|
||||
jekyll-gist (= 1.5.0)
|
||||
jekyll-github-metadata (= 2.13.0)
|
||||
jekyll-mentions (= 1.6.0)
|
||||
jekyll-optional-front-matter (= 0.3.2)
|
||||
jekyll-github-metadata (= 2.12.1)
|
||||
jekyll-mentions (= 1.4.1)
|
||||
jekyll-optional-front-matter (= 0.3.0)
|
||||
jekyll-paginate (= 1.1.0)
|
||||
jekyll-readme-index (= 0.3.0)
|
||||
jekyll-redirect-from (= 0.16.0)
|
||||
jekyll-relative-links (= 0.6.1)
|
||||
jekyll-remote-theme (= 0.4.2)
|
||||
jekyll-readme-index (= 0.2.0)
|
||||
jekyll-redirect-from (= 0.14.0)
|
||||
jekyll-relative-links (= 0.6.0)
|
||||
jekyll-remote-theme (= 0.4.0)
|
||||
jekyll-sass-converter (= 1.5.2)
|
||||
jekyll-seo-tag (= 2.6.1)
|
||||
jekyll-sitemap (= 1.4.0)
|
||||
jekyll-swiss (= 1.0.0)
|
||||
jekyll-seo-tag (= 2.5.0)
|
||||
jekyll-sitemap (= 1.2.0)
|
||||
jekyll-swiss (= 0.4.0)
|
||||
jekyll-theme-architect (= 0.1.1)
|
||||
jekyll-theme-cayman (= 0.1.1)
|
||||
jekyll-theme-dinky (= 0.1.1)
|
||||
jekyll-theme-hacker (= 0.1.2)
|
||||
jekyll-theme-hacker (= 0.1.1)
|
||||
jekyll-theme-leap-day (= 0.1.1)
|
||||
jekyll-theme-merlot (= 0.1.1)
|
||||
jekyll-theme-midnight (= 0.1.1)
|
||||
jekyll-theme-minimal (= 0.1.1)
|
||||
jekyll-theme-modernist (= 0.1.1)
|
||||
jekyll-theme-primer (= 0.5.4)
|
||||
jekyll-theme-primer (= 0.5.3)
|
||||
jekyll-theme-slate (= 0.1.1)
|
||||
jekyll-theme-tactile (= 0.1.1)
|
||||
jekyll-theme-time-machine (= 0.1.1)
|
||||
jekyll-titles-from-headings (= 0.5.3)
|
||||
jemoji (= 0.12.0)
|
||||
kramdown (= 2.3.0)
|
||||
kramdown-parser-gfm (= 1.1.0)
|
||||
liquid (= 4.0.3)
|
||||
jekyll-titles-from-headings (= 0.5.1)
|
||||
jemoji (= 0.10.2)
|
||||
kramdown (= 1.17.0)
|
||||
liquid (= 4.0.0)
|
||||
listen (= 3.1.5)
|
||||
mercenary (~> 0.3)
|
||||
minima (= 2.5.1)
|
||||
minima (= 2.5.0)
|
||||
nokogiri (>= 1.10.4, < 2.0)
|
||||
rouge (= 3.23.0)
|
||||
rouge (= 3.11.0)
|
||||
terminal-table (~> 1.4)
|
||||
github-pages-health-check (1.16.1)
|
||||
addressable (~> 2.3)
|
||||
@@ -84,27 +81,27 @@ GEM
|
||||
octokit (~> 4.0)
|
||||
public_suffix (~> 3.0)
|
||||
typhoeus (~> 1.3)
|
||||
html-pipeline (2.14.0)
|
||||
html-pipeline (2.12.0)
|
||||
activesupport (>= 2)
|
||||
nokogiri (>= 1.4)
|
||||
http_parser.rb (0.6.0)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jekyll (3.9.0)
|
||||
jekyll (3.8.5)
|
||||
addressable (~> 2.4)
|
||||
colorator (~> 1.0)
|
||||
em-websocket (~> 0.5)
|
||||
i18n (~> 0.7)
|
||||
jekyll-sass-converter (~> 1.0)
|
||||
jekyll-watch (~> 2.0)
|
||||
kramdown (>= 1.17, < 3)
|
||||
kramdown (~> 1.14)
|
||||
liquid (~> 4.0)
|
||||
mercenary (~> 0.3.3)
|
||||
pathutil (~> 0.9)
|
||||
rouge (>= 1.7, < 4)
|
||||
safe_yaml (~> 1.0)
|
||||
jekyll-avatar (0.7.0)
|
||||
jekyll (>= 3.0, < 5.0)
|
||||
jekyll-avatar (0.6.0)
|
||||
jekyll (~> 3.0)
|
||||
jekyll-coffeescript (1.1.1)
|
||||
coffee-script (~> 2.2)
|
||||
coffee-script-source (~> 1.11.1)
|
||||
@@ -117,37 +114,36 @@ GEM
|
||||
rouge (>= 2.0, < 4.0)
|
||||
jekyll-default-layout (0.1.4)
|
||||
jekyll (~> 3.0)
|
||||
jekyll-feed (0.15.1)
|
||||
jekyll (>= 3.7, < 5.0)
|
||||
jekyll-feed (0.11.0)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-gist (1.5.0)
|
||||
octokit (~> 4.2)
|
||||
jekyll-github-metadata (2.13.0)
|
||||
jekyll (>= 3.4, < 5.0)
|
||||
jekyll-github-metadata (2.12.1)
|
||||
jekyll (~> 3.4)
|
||||
octokit (~> 4.0, != 4.4.0)
|
||||
jekyll-mentions (1.6.0)
|
||||
jekyll-mentions (1.4.1)
|
||||
html-pipeline (~> 2.3)
|
||||
jekyll (>= 3.7, < 5.0)
|
||||
jekyll-optional-front-matter (0.3.2)
|
||||
jekyll (>= 3.0, < 5.0)
|
||||
jekyll (~> 3.0)
|
||||
jekyll-optional-front-matter (0.3.0)
|
||||
jekyll (~> 3.0)
|
||||
jekyll-paginate (1.1.0)
|
||||
jekyll-readme-index (0.3.0)
|
||||
jekyll (>= 3.0, < 5.0)
|
||||
jekyll-redirect-from (0.16.0)
|
||||
jekyll (>= 3.3, < 5.0)
|
||||
jekyll-relative-links (0.6.1)
|
||||
jekyll (>= 3.3, < 5.0)
|
||||
jekyll-remote-theme (0.4.2)
|
||||
jekyll-readme-index (0.2.0)
|
||||
jekyll (~> 3.0)
|
||||
jekyll-redirect-from (0.14.0)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-relative-links (0.6.0)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-remote-theme (0.4.0)
|
||||
addressable (~> 2.0)
|
||||
jekyll (>= 3.5, < 5.0)
|
||||
jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
|
||||
rubyzip (>= 1.3.0, < 3.0)
|
||||
jekyll (~> 3.5)
|
||||
rubyzip (>= 1.2.1, < 3.0)
|
||||
jekyll-sass-converter (1.5.2)
|
||||
sass (~> 3.4)
|
||||
jekyll-seo-tag (2.6.1)
|
||||
jekyll (>= 3.3, < 5.0)
|
||||
jekyll-sitemap (1.4.0)
|
||||
jekyll (>= 3.7, < 5.0)
|
||||
jekyll-swiss (1.0.0)
|
||||
jekyll-seo-tag (2.5.0)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-sitemap (1.2.0)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-swiss (0.4.0)
|
||||
jekyll-theme-architect (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
@@ -157,8 +153,8 @@ GEM
|
||||
jekyll-theme-dinky (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-theme-hacker (0.1.2)
|
||||
jekyll (> 3.5, < 5.0)
|
||||
jekyll-theme-hacker (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-theme-leap-day (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
@@ -175,8 +171,8 @@ GEM
|
||||
jekyll-theme-modernist (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-theme-primer (0.5.4)
|
||||
jekyll (> 3.5, < 5.0)
|
||||
jekyll-theme-primer (0.5.3)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-github-metadata (~> 2.9)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-theme-slate (0.1.1)
|
||||
@@ -188,49 +184,43 @@ GEM
|
||||
jekyll-theme-time-machine (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-titles-from-headings (0.5.3)
|
||||
jekyll (>= 3.3, < 5.0)
|
||||
jekyll-titles-from-headings (0.5.1)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-watch (2.2.1)
|
||||
listen (~> 3.0)
|
||||
jemoji (0.12.0)
|
||||
jemoji (0.10.2)
|
||||
gemoji (~> 3.0)
|
||||
html-pipeline (~> 2.2)
|
||||
jekyll (>= 3.0, < 5.0)
|
||||
kramdown (2.3.0)
|
||||
rexml
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
liquid (4.0.3)
|
||||
listen (3.4.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
jekyll (~> 3.0)
|
||||
kramdown (1.17.0)
|
||||
liquid (4.0.0)
|
||||
listen (3.1.5)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
ruby_dep (~> 1.2)
|
||||
mercenary (0.3.6)
|
||||
mini_portile2 (2.5.0)
|
||||
minima (2.5.1)
|
||||
jekyll (>= 3.5, < 5.0)
|
||||
mini_portile2 (2.4.0)
|
||||
minima (2.5.0)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-feed (~> 0.9)
|
||||
jekyll-seo-tag (~> 2.1)
|
||||
minitest (5.14.3)
|
||||
minitest (5.12.2)
|
||||
multipart-post (2.1.1)
|
||||
nokogiri (1.11.1)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
octokit (4.20.0)
|
||||
faraday (>= 0.9)
|
||||
nokogiri (1.10.8)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
octokit (4.14.0)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
pathutil (0.16.2)
|
||||
forwardable-extended (~> 2.6)
|
||||
public_suffix (3.1.1)
|
||||
racc (1.5.2)
|
||||
rb-fsevent (0.10.4)
|
||||
rb-inotify (0.10.1)
|
||||
rb-fsevent (0.10.3)
|
||||
rb-inotify (0.10.0)
|
||||
ffi (~> 1.0)
|
||||
rexml (3.2.4)
|
||||
rouge (3.23.0)
|
||||
ruby-enum (0.8.0)
|
||||
rouge (3.11.0)
|
||||
ruby-enum (0.7.2)
|
||||
i18n
|
||||
ruby2_keywords (0.0.2)
|
||||
rubyzip (2.3.0)
|
||||
ruby_dep (1.5.0)
|
||||
rubyzip (2.0.0)
|
||||
safe_yaml (1.0.5)
|
||||
sass (3.7.4)
|
||||
sass-listen (~> 4.0.0)
|
||||
@@ -240,20 +230,14 @@ GEM
|
||||
sawyer (0.8.2)
|
||||
addressable (>= 2.3.5)
|
||||
faraday (> 0.8, < 2.0)
|
||||
simpleidn (0.1.1)
|
||||
unf (~> 0.1.4)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
thread_safe (0.3.6)
|
||||
typhoeus (1.4.0)
|
||||
typhoeus (1.3.1)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (1.2.9)
|
||||
tzinfo (1.2.5)
|
||||
thread_safe (~> 0.1)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
zeitwerk (2.4.2)
|
||||
unicode-display_width (1.6.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@@ -263,4 +247,4 @@ DEPENDENCIES
|
||||
jekyll-sitemap
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.5
|
||||
1.17.2
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: kube-hunter
|
||||
description: Kube-hunter hunts for security weaknesses in Kubernetes clusters
|
||||
logo: https://raw.githubusercontent.com/aquasecurity/kube-hunter/main/kube-hunter.png
|
||||
logo: https://raw.githubusercontent.com/aquasecurity/kube-hunter/master/kube-hunter.png
|
||||
show_downloads: false
|
||||
google_analytics: UA-63272154-1
|
||||
theme: jekyll-theme-minimal
|
||||
@@ -11,7 +10,7 @@ collections:
|
||||
defaults:
|
||||
-
|
||||
scope:
|
||||
path: "" # an empty string here means all files in the project
|
||||
path: "" # an empty string here means all files in the project
|
||||
values:
|
||||
layout: "default"
|
||||
|
||||
|
||||
@@ -12,10 +12,7 @@ Microsoft Azure provides an internal HTTP endpoint that exposes information from
|
||||
|
||||
## Remediation
|
||||
|
||||
Starting in the 2020.10.15 Azure VHD Release, AKS restricts the pod CIDR access to that internal HTTP endpoint.
|
||||
|
||||
[CVE-2021-27075](https://github.com/Azure/AKS/issues/2168)
|
||||
|
||||
Consider using AAD Pod Identity. A Microsoft project that allows scoping the identity of workloads to Kubernetes Pods instead of VMs (instances).
|
||||
|
||||
## References
|
||||
|
||||
|
||||
9
job.yaml
9
job.yaml
@@ -1,4 +1,3 @@
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
@@ -7,9 +6,9 @@ spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: kube-hunter
|
||||
image: aquasec/kube-hunter
|
||||
command: ["kube-hunter"]
|
||||
args: ["--pod"]
|
||||
- name: kube-hunter
|
||||
image: aquasec/kube-hunter
|
||||
command: ["kube-hunter"]
|
||||
args: ["--pod"]
|
||||
restartPolicy: Never
|
||||
backoffLimit: 4
|
||||
|
||||
@@ -6,7 +6,7 @@ from threading import Thread
|
||||
|
||||
from kube_hunter.conf import get_config
|
||||
from kube_hunter.core.types import ActiveHunter, HunterBase
|
||||
from kube_hunter.core.events.types import Vulnerability, EventFilterBase, MultipleEventsContainer
|
||||
from kube_hunter.core.events.types import Vulnerability, EventFilterBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,32 +19,10 @@ class EventQueue(Queue):
|
||||
self.active_hunters = dict()
|
||||
self.all_hunters = dict()
|
||||
|
||||
self.running = True
|
||||
self.workers = list()
|
||||
|
||||
# -- Regular Subscription --
|
||||
# Structure: key: Event Class, value: tuple(Registered Hunter, Predicate Function)
|
||||
self.hooks = defaultdict(list)
|
||||
self.filters = defaultdict(list)
|
||||
# --------------------------
|
||||
|
||||
# -- Multiple Subscription --
|
||||
# Structure: key: Event Class, value: tuple(Registered Hunter, Predicate Function)
|
||||
self.multi_hooks = defaultdict(list)
|
||||
|
||||
# When subscribing to multiple events, this gets populated with required event classes
|
||||
# Structure: key: Hunter Class, value: set(RequiredEventClass1, RequiredEventClass2)
|
||||
self.hook_dependencies = defaultdict(set)
|
||||
|
||||
# To keep track of fulfilled dependencies. we need to have a structure which saves historical instanciated
|
||||
# events mapped to a registered hunter.
|
||||
# We used a 2 dimensional dictionary in order to fulfill two demands:
|
||||
# * correctly count published required events
|
||||
# * save historical events fired, easily sorted by their type
|
||||
#
|
||||
# Structure: hook_fulfilled_deps[hunter_class] -> fulfilled_events_for_hunter[event_class] -> [EventObject, EventObject2]
|
||||
self.hook_fulfilled_deps = defaultdict(lambda: defaultdict(list))
|
||||
# ---------------------------
|
||||
self.running = True
|
||||
self.workers = list()
|
||||
|
||||
for _ in range(num_worker):
|
||||
t = Thread(target=self.worker)
|
||||
@@ -56,66 +34,16 @@ class EventQueue(Queue):
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
"""
|
||||
######################################################
|
||||
+ ----------------- Public Methods ----------------- +
|
||||
######################################################
|
||||
"""
|
||||
|
||||
# decorator wrapping for easy subscription
|
||||
def subscribe(self, event, hook=None, predicate=None):
|
||||
"""
|
||||
The Subscribe Decorator - For Regular Registration
|
||||
Use this to register for one event only. Your hunter will execute each time this event is published
|
||||
|
||||
@param event - Event class to subscribe to
|
||||
@param predicate - Optional: Function that will be called with the published event as a parameter before trigger.
|
||||
If it's return value is False, the Hunter will not run (default=None).
|
||||
@param hook - Hunter class to register for (ignore when using as a decorator)
|
||||
"""
|
||||
|
||||
def wrapper(hook):
|
||||
self.subscribe_event(event, hook=hook, predicate=predicate)
|
||||
return hook
|
||||
|
||||
return wrapper
|
||||
|
||||
def subscribe_many(self, events, hook=None, predicates=None):
|
||||
"""
|
||||
The Subscribe Many Decorator - For Multiple Registration,
|
||||
When your attack needs several prerequisites to exist in the cluster, You need to register for multiple events.
|
||||
Your hunter will execute once for every new combination of required events.
|
||||
For example:
|
||||
1. event A was published 3 times
|
||||
2. event B was published once.
|
||||
3. event B was published again
|
||||
Your hunter will execute 2 times:
|
||||
* (on step 2) with the newest version of A
|
||||
* (on step 3) with the newest version of A and newest version of B
|
||||
|
||||
@param events - List of event classes to subscribe to
|
||||
@param predicates - Optional: List of function that will be called with the published event as a parameter before trigger.
|
||||
If it's return value is False, the Hunter will not run (default=None).
|
||||
@param hook - Hunter class to register for (ignore when using as a decorator)
|
||||
"""
|
||||
|
||||
def wrapper(hook):
|
||||
self.subscribe_events(events, hook=hook, predicates=predicates)
|
||||
return hook
|
||||
|
||||
return wrapper
|
||||
|
||||
# wrapper takes care of the subscribe once mechanism
|
||||
def subscribe_once(self, event, hook=None, predicate=None):
|
||||
"""
|
||||
The Subscribe Once Decorator - For Single Trigger Registration,
|
||||
Use this when you want your hunter to execute only in your entire program run
|
||||
wraps subscribe_event method
|
||||
|
||||
@param events - List of event classes to subscribe to
|
||||
@param predicates - Optional: List of function that will be called with the published event as a parameter before trigger.
|
||||
If it's return value is False, the Hunter will not run (default=None).
|
||||
@param hook - Hunter class to register for (ignore when using as a decorator)
|
||||
"""
|
||||
|
||||
def wrapper(hook):
|
||||
# installing a __new__ magic method on the hunter
|
||||
# which will remove the hunter from the list upon creation
|
||||
@@ -130,160 +58,29 @@ class EventQueue(Queue):
|
||||
|
||||
return wrapper
|
||||
|
||||
def publish_event(self, event, caller=None):
|
||||
"""
|
||||
The Publish Event Method - For Publishing Events To Kube-Hunter's Queue
|
||||
"""
|
||||
# Document that the hunter published a vulnerability (if it's indeed a vulnerability)
|
||||
# For statistics options
|
||||
self._increase_vuln_count(event, caller)
|
||||
|
||||
# sets the event's parent to be it's publisher hunter.
|
||||
self._set_event_chain(event, caller)
|
||||
|
||||
# applying filters on the event, before publishing it to subscribers.
|
||||
# if filter returned None, not proceeding to publish
|
||||
event = self.apply_filters(event)
|
||||
if event:
|
||||
# If event was rewritten, make sure it's linked again
|
||||
self._set_event_chain(event, caller)
|
||||
|
||||
# Regular Hunter registrations - publish logic
|
||||
# Here we iterate over all the registered-to events:
|
||||
for hooked_event in self.hooks.keys():
|
||||
# We check if the event we want to publish is an inherited class of the current registered-to iterated event
|
||||
# Meaning - if this is a relevant event:
|
||||
if hooked_event in event.__class__.__mro__:
|
||||
# If so, we want to publish to all registerd hunters.
|
||||
for hook, predicate in self.hooks[hooked_event]:
|
||||
if predicate and not predicate(event):
|
||||
continue
|
||||
|
||||
self.put(hook(event))
|
||||
logger.debug(f"Event {event.__class__} got published to hunter - {hook} with {event}")
|
||||
|
||||
# Multiple Hunter registrations - publish logic
|
||||
# Here we iterate over all the registered-to events:
|
||||
for hooked_event in self.multi_hooks.keys():
|
||||
# We check if the event we want to publish is an inherited class of the current registered-to iterated event
|
||||
# Meaning - if this is a relevant event:
|
||||
if hooked_event in event.__class__.__mro__:
|
||||
# now we iterate over the corresponding registered hunters.
|
||||
for hook, predicate in self.multi_hooks[hooked_event]:
|
||||
if predicate and not predicate(event):
|
||||
continue
|
||||
|
||||
self._update_multi_hooks(hook, event)
|
||||
|
||||
if self._is_all_fulfilled_for_hunter(hook):
|
||||
events_container = MultipleEventsContainer(self._get_latest_events_from_multi_hooks(hook))
|
||||
self.put(hook(events_container))
|
||||
logger.debug(
|
||||
f"Multiple subscription requirements were met for hunter {hook}. events container was \
|
||||
published with {self.hook_fulfilled_deps[hook].keys()}"
|
||||
)
|
||||
|
||||
"""
|
||||
######################################################
|
||||
+ ---------------- Private Methods ----------------- +
|
||||
+ ---------------- (Backend Logic) ----------------- +
|
||||
######################################################
|
||||
"""
|
||||
|
||||
def _get_latest_events_from_multi_hooks(self, hook):
|
||||
"""
|
||||
Iterates over fulfilled deps for the hunter, and fetching the latest appended events from history
|
||||
"""
|
||||
latest_events = list()
|
||||
for event_class in self.hook_fulfilled_deps[hook].keys():
|
||||
latest_events.append(self.hook_fulfilled_deps[hook][event_class][-1])
|
||||
return latest_events
|
||||
|
||||
def _update_multi_hooks(self, hook, event):
|
||||
"""
|
||||
Updates published events in the multi hooks fulfilled store.
|
||||
"""
|
||||
self.hook_fulfilled_deps[hook][event.__class__].append(event)
|
||||
|
||||
def _is_all_fulfilled_for_hunter(self, hook):
|
||||
"""
|
||||
Returns true for multi hook fulfilled, else oterwise
|
||||
"""
|
||||
# Check if the first dimension already contains all necessary event classes
|
||||
return len(self.hook_fulfilled_deps[hook].keys()) == len(self.hook_dependencies[hook])
|
||||
|
||||
def _set_event_chain(self, event, caller):
|
||||
"""
|
||||
Sets' events attribute chain.
|
||||
In here we link the event with it's publisher (Hunter),
|
||||
so in the next hunter that catches this event, we could access the previous one's attributes.
|
||||
|
||||
@param event: the event object to be chained
|
||||
@param caller: the Hunter object that published this event.
|
||||
"""
|
||||
if caller:
|
||||
event.previous = caller.event
|
||||
event.hunter = caller.__class__
|
||||
|
||||
def _register_hunters(self, hook=None):
|
||||
"""
|
||||
This method is called when a Hunter registers itself to the handler.
|
||||
this is done in order to track and correctly configure the current run of the program.
|
||||
|
||||
passive_hunters, active_hunters, all_hunters
|
||||
"""
|
||||
# getting uninstantiated event object
|
||||
def subscribe_event(self, event, hook=None, predicate=None):
|
||||
config = get_config()
|
||||
if ActiveHunter in hook.__mro__:
|
||||
if not config.active:
|
||||
return False
|
||||
else:
|
||||
self.active_hunters[hook] = hook.__doc__
|
||||
return
|
||||
self.active_hunters[hook] = hook.__doc__
|
||||
elif HunterBase in hook.__mro__:
|
||||
self.passive_hunters[hook] = hook.__doc__
|
||||
|
||||
if HunterBase in hook.__mro__:
|
||||
self.all_hunters[hook] = hook.__doc__
|
||||
|
||||
return True
|
||||
|
||||
def _register_filter(self, event, hook=None, predicate=None):
|
||||
if hook not in self.filters[event]:
|
||||
self.filters[event].append((hook, predicate))
|
||||
logging.debug("{} filter subscribed to {}".format(hook, event))
|
||||
|
||||
def _register_hook(self, event, hook=None, predicate=None):
|
||||
if hook not in self.hooks[event]:
|
||||
self.hooks[event].append((hook, predicate))
|
||||
logging.debug("{} subscribed to {}".format(hook, event))
|
||||
|
||||
def subscribe_event(self, event, hook=None, predicate=None):
|
||||
if not self._register_hunters(hook):
|
||||
return
|
||||
|
||||
# registering filters
|
||||
if EventFilterBase in hook.__mro__:
|
||||
self._register_filter(event, hook, predicate)
|
||||
if hook not in self.filters[event]:
|
||||
self.filters[event].append((hook, predicate))
|
||||
logger.debug(f"{hook} filter subscribed to {event}")
|
||||
|
||||
# registering hunters
|
||||
else:
|
||||
self._register_hook(event, hook, predicate)
|
||||
|
||||
def subscribe_events(self, events, hook=None, predicates=None):
|
||||
if not self._register_hunters(hook):
|
||||
return False
|
||||
|
||||
if predicates is None:
|
||||
predicates = [None] * len(events)
|
||||
|
||||
# registering filters.
|
||||
if EventFilterBase in hook.__mro__:
|
||||
for event, predicate in zip(events, predicates):
|
||||
self._register_filter(event, hook, predicate)
|
||||
# registering hunters.
|
||||
else:
|
||||
for event, predicate in zip(events, predicates):
|
||||
self.multi_hooks[event].append((hook, predicate))
|
||||
|
||||
self.hook_dependencies[hook] = frozenset(events)
|
||||
elif hook not in self.hooks[event]:
|
||||
self.hooks[event].append((hook, predicate))
|
||||
logger.debug(f"{hook} subscribed to {event}")
|
||||
|
||||
def apply_filters(self, event):
|
||||
# if filters are subscribed, apply them on the event
|
||||
@@ -300,11 +97,36 @@ class EventQueue(Queue):
|
||||
return None
|
||||
return event
|
||||
|
||||
def _increase_vuln_count(self, event, caller):
|
||||
# getting instantiated event object
|
||||
def publish_event(self, event, caller=None):
|
||||
config = get_config()
|
||||
if config.statistics and caller:
|
||||
if Vulnerability in event.__class__.__mro__:
|
||||
caller.__class__.publishedVulnerabilities += 1
|
||||
|
||||
# setting event chain
|
||||
if caller:
|
||||
event.previous = caller.event
|
||||
event.hunter = caller.__class__
|
||||
|
||||
# applying filters on the event, before publishing it to subscribers.
|
||||
# if filter returned None, not proceeding to publish
|
||||
event = self.apply_filters(event)
|
||||
if event:
|
||||
# If event was rewritten, make sure it's linked to its parent ('previous') event
|
||||
if caller:
|
||||
event.previous = caller.event
|
||||
event.hunter = caller.__class__
|
||||
|
||||
for hooked_event in self.hooks.keys():
|
||||
if hooked_event in event.__class__.__mro__:
|
||||
for hook, predicate in self.hooks[hooked_event]:
|
||||
if predicate and not predicate(event):
|
||||
continue
|
||||
|
||||
if config.statistics and caller:
|
||||
if Vulnerability in event.__class__.__mro__:
|
||||
caller.__class__.publishedVulnerabilities += 1
|
||||
|
||||
logger.debug(f"Event {event.__class__} got published with {event}")
|
||||
self.put(hook(event))
|
||||
|
||||
# executes callbacks on dedicated thread as a daemon
|
||||
def worker(self):
|
||||
|
||||
@@ -62,20 +62,6 @@ class Event:
|
||||
return history
|
||||
|
||||
|
||||
class MultipleEventsContainer(Event):
|
||||
"""
|
||||
This is the class of the object an hunter will get if he was registered to multiple events.
|
||||
"""
|
||||
|
||||
def __init__(self, events):
|
||||
self.events = events
|
||||
|
||||
def get_by_class(self, event_class):
|
||||
for event in self.events:
|
||||
if event.__class__ == event_class:
|
||||
return event
|
||||
|
||||
|
||||
class Service:
|
||||
def __init__(self, name, path="", secure=True):
|
||||
self.name = name
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from kube_hunter.conf import get_config
|
||||
from kube_hunter.modules.hunting.kubelet import ExposedPodsHandler, SecureKubeletPortHunter
|
||||
from kube_hunter.modules.hunting.kubelet import ExposedRunHandler
|
||||
from kube_hunter.core.events import handler
|
||||
from kube_hunter.core.events.types import Event, Vulnerability
|
||||
from kube_hunter.core.types import Hunter, ActiveHunter, IdentityTheft, Azure
|
||||
@@ -15,7 +14,7 @@ logger = logging.getLogger(__name__)
|
||||
class AzureSpnExposure(Vulnerability, Event):
|
||||
"""The SPN is exposed, potentially allowing an attacker to gain access to the Azure subscription"""
|
||||
|
||||
def __init__(self, container, evidence=""):
|
||||
def __init__(self, container):
|
||||
Vulnerability.__init__(
|
||||
self,
|
||||
Azure,
|
||||
@@ -24,10 +23,9 @@ class AzureSpnExposure(Vulnerability, Event):
|
||||
vid="KHV004",
|
||||
)
|
||||
self.container = container
|
||||
self.evidence = evidence
|
||||
|
||||
|
||||
@handler.subscribe(ExposedPodsHandler, predicate=lambda x: x.cloud_type == "Azure")
|
||||
@handler.subscribe(ExposedRunHandler, predicate=lambda x: x.cloud == "Azure")
|
||||
class AzureSpnHunter(Hunter):
|
||||
"""AKS Hunting
|
||||
Hunting Azure cluster deployments using specific known configurations
|
||||
@@ -39,33 +37,35 @@ class AzureSpnHunter(Hunter):
|
||||
|
||||
# getting a container that has access to the azure.json file
|
||||
def get_key_container(self):
|
||||
config = get_config()
|
||||
endpoint = f"{self.base_url}/pods"
|
||||
logger.debug("Trying to find container with access to azure.json file")
|
||||
|
||||
# pods are saved in the previous event object
|
||||
pods_data = self.event.pods
|
||||
|
||||
suspicious_volume_names = []
|
||||
for pod_data in pods_data:
|
||||
for volume in pod_data["spec"].get("volumes", []):
|
||||
if volume.get("hostPath"):
|
||||
path = volume["hostPath"]["path"]
|
||||
if "/etc/kubernetes/azure.json".startswith(path):
|
||||
suspicious_volume_names.append(volume["name"])
|
||||
for container in pod_data["spec"]["containers"]:
|
||||
for mount in container.get("volumeMounts", []):
|
||||
if mount["name"] in suspicious_volume_names:
|
||||
return {
|
||||
"name": container["name"],
|
||||
"pod": pod_data["metadata"]["name"],
|
||||
"namespace": pod_data["metadata"]["namespace"],
|
||||
"mount": mount,
|
||||
}
|
||||
try:
|
||||
r = requests.get(endpoint, verify=False, timeout=config.network_timeout)
|
||||
except requests.Timeout:
|
||||
logger.debug("failed getting pod info")
|
||||
else:
|
||||
pods_data = r.json().get("items", [])
|
||||
suspicious_volume_names = []
|
||||
for pod_data in pods_data:
|
||||
for volume in pod_data["spec"].get("volumes", []):
|
||||
if volume.get("hostPath"):
|
||||
path = volume["hostPath"]["path"]
|
||||
if "/etc/kubernetes/azure.json".startswith(path):
|
||||
suspicious_volume_names.append(volume["name"])
|
||||
for container in pod_data["spec"]["containers"]:
|
||||
for mount in container.get("volumeMounts", []):
|
||||
if mount["name"] in suspicious_volume_names:
|
||||
return {
|
||||
"name": container["name"],
|
||||
"pod": pod_data["metadata"]["name"],
|
||||
"namespace": pod_data["metadata"]["namespace"],
|
||||
}
|
||||
|
||||
def execute(self):
|
||||
container = self.get_key_container()
|
||||
if container:
|
||||
evidence = f"pod: {container['pod']}, namespace: {container['namespace']}"
|
||||
self.publish_event(AzureSpnExposure(container=container, evidence=evidence))
|
||||
self.publish_event(AzureSpnExposure(container=container))
|
||||
|
||||
|
||||
@handler.subscribe(AzureSpnExposure)
|
||||
@@ -78,42 +78,14 @@ class ProveAzureSpnExposure(ActiveHunter):
|
||||
self.event = event
|
||||
self.base_url = f"https://{self.event.host}:{self.event.port}"
|
||||
|
||||
def test_run_capability(self):
|
||||
"""
|
||||
Uses SecureKubeletPortHunter to test the /run handler
|
||||
TODO: when multiple event subscription is implemented, use this here to make sure /run is accessible
|
||||
"""
|
||||
debug_handlers = SecureKubeletPortHunter.DebugHandlers(path=self.base_url, session=self.event.session, pod=None)
|
||||
return debug_handlers.test_run_container()
|
||||
|
||||
def run(self, command, container):
|
||||
config = get_config()
|
||||
run_url = f"{self.base_url}/run/{container['namespace']}/{container['pod']}/{container['name']}"
|
||||
return self.event.session.post(run_url, verify=False, params={"cmd": command}, timeout=config.network_timeout)
|
||||
|
||||
def get_full_path_to_azure_file(self):
|
||||
"""
|
||||
Returns a full path to /etc/kubernetes/azure.json
|
||||
Taking into consideration the difference folder of the mount inside the container.
|
||||
TODO: implement the edge case where the mount is to parent /etc folder.
|
||||
"""
|
||||
azure_file_path = self.event.container["mount"]["mountPath"]
|
||||
|
||||
# taking care of cases where a subPath is added to map the specific file
|
||||
if not azure_file_path.endswith("azure.json"):
|
||||
azure_file_path = os.path.join(azure_file_path, "azure.json")
|
||||
|
||||
return azure_file_path
|
||||
run_url = "/".join(self.base_url, "run", container["namespace"], container["pod"], container["name"])
|
||||
return requests.post(run_url, verify=False, params={"cmd": command}, timeout=config.network_timeout)
|
||||
|
||||
def execute(self):
|
||||
if not self.test_run_capability():
|
||||
logger.debug("Not proving AzureSpnExposure because /run debug handler is disabled")
|
||||
return
|
||||
|
||||
try:
|
||||
azure_file_path = self.get_full_path_to_azure_file()
|
||||
logger.debug(f"trying to access the azure.json at the resolved path: {azure_file_path}")
|
||||
subscription = self.run(f"cat {azure_file_path}", container=self.event.container).json()
|
||||
subscription = self.run("cat /etc/kubernetes/azure.json", container=self.event.container).json()
|
||||
except requests.Timeout:
|
||||
logger.debug("failed to run command in container", exc_info=True)
|
||||
except json.decoder.JSONDecodeError:
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
datas, binaries, hiddenimports = collect_all("prettytable")
|
||||
2
setup.py
2
setup.py
@@ -41,8 +41,6 @@ class PyInstallerCommand(Command):
|
||||
cfg.read("setup.cfg")
|
||||
command = [
|
||||
"pyinstaller",
|
||||
"--additional-hooks-dir",
|
||||
"pyinstaller_hooks",
|
||||
"--clean",
|
||||
"--onefile",
|
||||
"--name",
|
||||
|
||||
@@ -6,8 +6,6 @@ from kube_hunter.core.events.types import Event, Service
|
||||
from kube_hunter.core.events import handler
|
||||
|
||||
counter = 0
|
||||
first_run = True
|
||||
|
||||
set_config(Config())
|
||||
|
||||
|
||||
@@ -21,16 +19,6 @@ class RegularEvent(Service, Event):
|
||||
Service.__init__(self, "Test Service")
|
||||
|
||||
|
||||
class AnotherRegularEvent(Service, Event):
|
||||
def __init__(self):
|
||||
Service.__init__(self, "Test Service (another)")
|
||||
|
||||
|
||||
class DifferentRegularEvent(Service, Event):
|
||||
def __init__(self):
|
||||
Service.__init__(self, "Test Service (different)")
|
||||
|
||||
|
||||
@handler.subscribe_once(OnceOnlyEvent)
|
||||
class OnceHunter(Hunter):
|
||||
def __init__(self, event):
|
||||
@@ -45,36 +33,8 @@ class RegularHunter(Hunter):
|
||||
counter += 1
|
||||
|
||||
|
||||
@handler.subscribe_many([DifferentRegularEvent, AnotherRegularEvent])
|
||||
class SmartHunter(Hunter):
|
||||
def __init__(self, events):
|
||||
global counter, first_run
|
||||
counter += 1
|
||||
|
||||
# we add an attribute on the second scan.
|
||||
# here we test that we get the latest event
|
||||
different_event = events.get_by_class(DifferentRegularEvent)
|
||||
if first_run:
|
||||
first_run = False
|
||||
assert not different_event.new_value
|
||||
else:
|
||||
assert different_event.new_value
|
||||
|
||||
|
||||
@handler.subscribe_many([DifferentRegularEvent, AnotherRegularEvent])
|
||||
class SmartHunter2(Hunter):
|
||||
def __init__(self, events):
|
||||
global counter
|
||||
counter += 1
|
||||
|
||||
# check if we can access the events
|
||||
assert events.get_by_class(DifferentRegularEvent).__class__ == DifferentRegularEvent
|
||||
assert events.get_by_class(AnotherRegularEvent).__class__ == AnotherRegularEvent
|
||||
|
||||
|
||||
def test_subscribe_mechanism():
|
||||
global counter
|
||||
counter = 0
|
||||
|
||||
# first test normal subscribe and publish works
|
||||
handler.publish_event(RegularEvent())
|
||||
@@ -83,47 +43,13 @@ def test_subscribe_mechanism():
|
||||
|
||||
time.sleep(0.02)
|
||||
assert counter == 3
|
||||
|
||||
|
||||
def test_subscribe_once_mechanism():
|
||||
global counter
|
||||
counter = 0
|
||||
|
||||
# testing the multiple subscription mechanism
|
||||
# testing the subscribe_once mechanism
|
||||
handler.publish_event(OnceOnlyEvent())
|
||||
handler.publish_event(OnceOnlyEvent())
|
||||
handler.publish_event(OnceOnlyEvent())
|
||||
|
||||
time.sleep(0.02)
|
||||
# should have been triggered once
|
||||
assert counter == 1
|
||||
counter = 0
|
||||
|
||||
handler.publish_event(OnceOnlyEvent())
|
||||
handler.publish_event(OnceOnlyEvent())
|
||||
handler.publish_event(OnceOnlyEvent())
|
||||
time.sleep(0.02)
|
||||
|
||||
assert counter == 0
|
||||
|
||||
|
||||
def test_subscribe_many_mechanism():
|
||||
global counter
|
||||
counter = 0
|
||||
|
||||
# testing the multiple subscription mechanism
|
||||
handler.publish_event(DifferentRegularEvent())
|
||||
handler.publish_event(DifferentRegularEvent())
|
||||
handler.publish_event(DifferentRegularEvent())
|
||||
handler.publish_event(DifferentRegularEvent())
|
||||
handler.publish_event(DifferentRegularEvent())
|
||||
handler.publish_event(AnotherRegularEvent())
|
||||
|
||||
time.sleep(0.02)
|
||||
# We expect SmartHunter and SmartHunter2 to be executed once. hence the counter should be 2
|
||||
assert counter == 2
|
||||
counter = 0
|
||||
|
||||
# Test using most recent event
|
||||
newer_version_event = DifferentRegularEvent()
|
||||
newer_version_event.new_value = True
|
||||
handler.publish_event(newer_version_event)
|
||||
|
||||
assert counter == 2
|
||||
|
||||
@@ -3,47 +3,54 @@ import requests_mock
|
||||
|
||||
from kube_hunter.conf import Config, set_config
|
||||
|
||||
import json
|
||||
|
||||
set_config(Config())
|
||||
|
||||
from kube_hunter.modules.hunting.kubelet import ExposedPodsHandler
|
||||
from kube_hunter.modules.hunting.kubelet import ExposedRunHandler
|
||||
from kube_hunter.modules.hunting.aks import AzureSpnHunter
|
||||
|
||||
|
||||
def test_AzureSpnHunter():
|
||||
e = ExposedPodsHandler(pods=[])
|
||||
e = ExposedRunHandler()
|
||||
e.host = "mockKubernetes"
|
||||
e.port = 443
|
||||
e.protocol = "https"
|
||||
|
||||
pod_template = '{{"items":[ {{"apiVersion":"v1","kind":"Pod","metadata":{{"name":"etc","namespace":"default"}},"spec":{{"containers":[{{"command":["sleep","99999"],"image":"ubuntu","name":"test","volumeMounts":[{{"mountPath":"/mp","name":"v"}}]}}],"volumes":[{{"hostPath":{{"path":"{}"}},"name":"v"}}]}}}} ]}}'
|
||||
|
||||
bad_paths = ["/", "/etc", "/etc/", "/etc/kubernetes", "/etc/kubernetes/azure.json"]
|
||||
good_paths = ["/yo", "/etc/yo", "/etc/kubernetes/yo.json"]
|
||||
|
||||
for p in bad_paths:
|
||||
e.pods = json.loads(pod_template.format(p))["items"]
|
||||
h = AzureSpnHunter(e)
|
||||
c = h.get_key_container()
|
||||
assert c
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get("https://mockKubernetes:443/pods", text=pod_template.format(p))
|
||||
h = AzureSpnHunter(e)
|
||||
c = h.get_key_container()
|
||||
assert c
|
||||
|
||||
for p in good_paths:
|
||||
e.pods = json.loads(pod_template.format(p))["items"]
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get("https://mockKubernetes:443/pods", text=pod_template.format(p))
|
||||
h = AzureSpnHunter(e)
|
||||
c = h.get_key_container()
|
||||
assert c == None
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
pod_no_volume_mounts = '{"items":[ {"apiVersion":"v1","kind":"Pod","metadata":{"name":"etc","namespace":"default"},"spec":{"containers":[{"command":["sleep","99999"],"image":"ubuntu","name":"test"}],"volumes":[{"hostPath":{"path":"/whatever"},"name":"v"}]}} ]}'
|
||||
m.get("https://mockKubernetes:443/pods", text=pod_no_volume_mounts)
|
||||
h = AzureSpnHunter(e)
|
||||
c = h.get_key_container()
|
||||
assert c == None
|
||||
|
||||
pod_no_volume_mounts = '{"items":[ {"apiVersion":"v1","kind":"Pod","metadata":{"name":"etc","namespace":"default"},"spec":{"containers":[{"command":["sleep","99999"],"image":"ubuntu","name":"test"}],"volumes":[{"hostPath":{"path":"/whatever"},"name":"v"}]}} ]}'
|
||||
e.pods = json.loads(pod_no_volume_mounts)["items"]
|
||||
h = AzureSpnHunter(e)
|
||||
c = h.get_key_container()
|
||||
assert c == None
|
||||
with requests_mock.Mocker() as m:
|
||||
pod_no_volumes = '{"items":[ {"apiVersion":"v1","kind":"Pod","metadata":{"name":"etc","namespace":"default"},"spec":{"containers":[{"command":["sleep","99999"],"image":"ubuntu","name":"test"}]}} ]}'
|
||||
m.get("https://mockKubernetes:443/pods", text=pod_no_volumes)
|
||||
h = AzureSpnHunter(e)
|
||||
c = h.get_key_container()
|
||||
assert c == None
|
||||
|
||||
pod_no_volumes = '{"items":[ {"apiVersion":"v1","kind":"Pod","metadata":{"name":"etc","namespace":"default"},"spec":{"containers":[{"command":["sleep","99999"],"image":"ubuntu","name":"test"}]}} ]}'
|
||||
e.pods = json.loads(pod_no_volumes)["items"]
|
||||
h = AzureSpnHunter(e)
|
||||
c = h.get_key_container()
|
||||
assert c == None
|
||||
|
||||
pod_other_volume = '{"items":[ {"apiVersion":"v1","kind":"Pod","metadata":{"name":"etc","namespace":"default"},"spec":{"containers":[{"command":["sleep","99999"],"image":"ubuntu","name":"test","volumeMounts":[{"mountPath":"/mp","name":"v"}]}],"volumes":[{"emptyDir":{},"name":"v"}]}} ]}'
|
||||
e.pods = json.loads(pod_other_volume)["items"]
|
||||
h = AzureSpnHunter(e)
|
||||
c = h.get_key_container()
|
||||
assert c == None
|
||||
with requests_mock.Mocker() as m:
|
||||
pod_other_volume = '{"items":[ {"apiVersion":"v1","kind":"Pod","metadata":{"name":"etc","namespace":"default"},"spec":{"containers":[{"command":["sleep","99999"],"image":"ubuntu","name":"test","volumeMounts":[{"mountPath":"/mp","name":"v"}]}],"volumes":[{"emptyDir":{},"name":"v"}]}} ]}'
|
||||
m.get("https://mockKubernetes:443/pods", text=pod_other_volume)
|
||||
h = AzureSpnHunter(e)
|
||||
c = h.get_key_container()
|
||||
assert c == None
|
||||
|
||||
Reference in New Issue
Block a user