├── todo ├── .gitignore ├── svachal01.png ├── svachal02.png ├── svachal03.png ├── svachal04.png ├── top_ports.png ├── top_services.png ├── top_categories.png ├── top_protocols.png ├── requirements.txt ├── completions.bash-completion ├── readme.md ├── template.writeup.yml ├── template.writeup.md ├── template.readme.md ├── yml2dot.py ├── utils.py └── svachal.py /todo: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | 3 | -------------------------------------------------------------------------------- /svachal01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7h3rAm/svachal/HEAD/svachal01.png -------------------------------------------------------------------------------- /svachal02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7h3rAm/svachal/HEAD/svachal02.png -------------------------------------------------------------------------------- /svachal03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7h3rAm/svachal/HEAD/svachal03.png -------------------------------------------------------------------------------- /svachal04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7h3rAm/svachal/HEAD/svachal04.png -------------------------------------------------------------------------------- /top_ports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7h3rAm/svachal/HEAD/top_ports.png -------------------------------------------------------------------------------- /top_services.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7h3rAm/svachal/HEAD/top_services.png -------------------------------------------------------------------------------- /top_categories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7h3rAm/svachal/HEAD/top_categories.png -------------------------------------------------------------------------------- /top_protocols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7h3rAm/svachal/HEAD/top_protocols.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | markdown2 2 | pyyaml 3 | jinja2 4 | requests 5 | pysparklines 6 | prettytable 7 | bs4 8 | matplotlib 9 | -------------------------------------------------------------------------------- /completions.bash-completion: -------------------------------------------------------------------------------- 1 | #/usr/bin/env bash 2 | 3 | complete -W "-h --help -w --writeupdir -g --githubrepourl -s --start -f --finish -r --rebuildall -z --summarize" svachal 4 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # svachal 2 | 3 | [![License: CC BY-SA 4.0](https://raw.githubusercontent.com/7h3rAm/7h3rAm.github.io/master/static/files/ccbysa4.svg)](https://creativecommons.org/licenses/by-sa/4.0/) 4 | 5 | This is an automation framework for machine writeups. It defines a YAML based writeup template that can be used while working on a machine. Once the writeup is complete, the YAML writeup file can be used to render a `.md` and `.pdf` report along with stats and summary for all completed writeups. It works in conjunction with [machinescli](https://github.com/7h3rAm/machinescli) project, so all machine metadata is natively accessible: 6 | 7 | ## Installation 8 | 9 | You will need to configure [`machinescli`](https://github.com/7h3rAm/machinescli) before using `svachal`. Follow [installation](https://github.com/7h3rAm/machinescli#installation) guide to create the shared `machines.json` file using `machinescli`: 10 | 11 | Next, clone `svachal` repository and install requirements: 12 | 13 | ``` 14 | $ cd $HOME/toolbox/projects 15 | $ git clone https://github.com/7h3rAm/svachal && cd svachal 16 | $ python3 -m venv --copies venv 17 | $ source venv/bin/activate 18 | $ pip install -r requirements.txt 19 | $ python3 svachal.py -h 20 | ``` 21 | 22 | `svachal` expects GitHub to be the primary portal for writeups storage and sharing. As such, it needs a repo URL for links within writeup `yml`/`md`/`pdf` files to correctly point to right resources. Initialize `svachal` for first run by creating a writeups directory and run with `-w` and `-g` arguments: 23 | 24 | ``` 25 | $ mkdir -pv $HOME/toolbox/projects/writeups && cd $HOME/toolbox/projects/writeups 26 | $ git init 27 | $ git add . 28 | $ git commit -m 'Initial commit' 29 | $ git remote add github http://github.com//writeups 30 | $ git push github master 31 | $ python3 $HOME/toolbox/projects/svachal.py -w $HOME/toolbox/projects/writeups -g http://github.com//writeups -s "https://app.hackthebox.eu/machines/200" 32 | writeup: 33 | metadata: 34 | status: private 35 | datetime: 20220101 36 | infra: HackTheBox 37 | name: Rope 38 | points: 50 39 | path: htb.rope 40 | url: https://app.hackthebox.eu/machines/200 41 | infocard: ./infocard.png 42 | references: 43 | - 44 | categories: 45 | - linux 46 | - hackthebox 47 | tags: 48 | - enumerate_ 49 | - exploit_ 50 | - privesc_ 51 | overview: 52 | description: | 53 | This is a writeup for HackTheBox VM [`Rope`](https://app.hackthebox.eu/machines/200). Here's an overview of the `enumeration` → `exploitation` → `privilege escalation` process: 54 | 55 | [+] writeup file '/home/kali/toolbox/projects/writeups/htb.rope/writeup.yml' created for target 'htb.rope' 56 | [+] created '/home/kali/toolbox/projects/writeups/htb.rope/ratings.png' file for target 'htb.rope' 57 | [+] created '/home/kali/toolbox/projects/writeups/htb.rope/matrix.png' file for target 'htb.rope' 58 | ``` 59 | 60 | ## Usage 61 | ![Usage](svachal01.png) 62 | 63 | ## Usecases 64 | 1. Start a new writeup: 65 | ![Start](svachal02.png) 66 | 67 | 1. Finish a writeup: 68 | ![Finish](svachal03.png) 69 | 70 | 1. Summarize all writeups: 71 | ![Summarize](svachal04.png) 72 | 73 | 1. Override default writeup directory and GitHub repo URL: 74 | ```console 75 | $ svachal -w $HOME/ -g "https://github.com// 76 | ``` 77 | 78 | ## Summarized Writeup Graphs 79 | 80 | ![Top writeup categories](top_categories.png) 81 | 82 | ![Top writeup ports](top_ports.png) 83 | 84 | ![Top writeup protocols](top_protocols.png) 85 | 86 | ![Top writeup services](top_services.png) 87 | 88 | 89 | ## Argument Autocomplete 90 | Source the `.bash-completion` file within a shell to trigger auto-complete for arguments. This will require the following alias: 91 | ```console 92 | alias svachal='python3 $HOME/toolbox/projects/svachal/svachal.py -w $HOME/toolbox/projects/writeups -g http://github.com//writeups' 93 | ``` 94 | 95 | > You will need a [Nerd Fonts patched font](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts) for OS icons and other symbols to be rendered correctly. 96 | -------------------------------------------------------------------------------- /template.writeup.yml: -------------------------------------------------------------------------------- 1 | writeup: 2 | metadata: 3 | status: private 4 | datetime: 20200220 5 | infra: 6 | name: 7 | points: 8 | path: 9 | url: 10 | infocard: ./infocard.png 11 | references: 12 | - 13 | categories: 14 | - oscp 15 | - vulnhub/hackthebox/tryhackme 16 | - linux/windows/bsd 17 | tags: 18 | - enumerate_ 19 | - exploit_ 20 | - privesc_ 21 | overview: 22 | description: | 23 | This is a writeup for VM [](). 24 | 25 | killchain: 26 | - "_ [] /": 27 | - ". a": 28 | - ". b": 29 | ttps: 30 | enumeration: 31 | steps: 32 | - description: | 33 | Here's the Nmap scan result: 34 | command: | 35 | - description: | 36 | Here's the summary of open ports and associated [AutoRecon](https://github.com/Tib3rius/AutoRecon) scan files: 37 | screenshot: 38 | - ./openports.png 39 | - description: | 40 | command: | 41 | screenshot: 42 | - ./screenshot00.png 43 | - description: | 44 | command: | 45 | screenshot: 46 | - ./screenshot00.png 47 | - description: | 48 | command: | 49 | screenshot: 50 | - ./screenshot00.png 51 | - description: | 52 | command: | 53 | screenshot: 54 | - ./screenshot00.png 55 | - description: | 56 | command: | 57 | screenshot: 58 | - ./screenshot00.png 59 | - description: | 60 | command: | 61 | screenshot: 62 | - ./screenshot00.png 63 | - description: | 64 | command: | 65 | screenshot: 66 | - ./screenshot00.png 67 | - description: | 68 | command: | 69 | screenshot: 70 | - ./screenshot00.png 71 | findings: 72 | openports: 73 | - 74 | files: 75 | - 76 | users: 77 | ftp: 78 | - 79 | ssh: 80 | - 81 | wordpress: 82 | - 83 | exploitation: 84 | steps: 85 | - description: | 86 | command: | 87 | screenshot: 88 | - ./screenshot00.png 89 | - description: | 90 | command: | 91 | screenshot: 92 | - ./screenshot00.png 93 | - description: | 94 | command: | 95 | screenshot: 96 | - ./screenshot00.png 97 | - description: | 98 | command: | 99 | screenshot: 100 | - ./screenshot00.png 101 | - description: | 102 | command: | 103 | screenshot: 104 | - ./screenshot00.png 105 | - description: | 106 | command: | 107 | screenshot: 108 | - ./screenshot00.png 109 | - description: | 110 | command: | 111 | screenshot: 112 | - ./screenshot00.png 113 | - description: | 114 | command: | 115 | screenshot: 116 | - ./screenshot00.png 117 | - description: | 118 | command: | 119 | screenshot: 120 | - ./screenshot00.png 121 | - description: | 122 | command: | 123 | screenshot: 124 | - ./screenshot00.png 125 | vuln: 126 | - cve: 127 | edb: 128 | links: 129 | - 130 | postexploit: 131 | user: 132 | hostname: 133 | id: | 134 | uname: | 135 | ifconfig: | 136 | users: 137 | - 138 | privesc: 139 | steps: 140 | - description: | 141 | command: | 142 | screenshot: 143 | - ./screenshot00.png 144 | - description: | 145 | command: | 146 | screenshot: 147 | - ./screenshot00.png 148 | - description: | 149 | command: | 150 | screenshot: 151 | - ./screenshot00.png 152 | - description: | 153 | command: | 154 | screenshot: 155 | - ./screenshot00.png 156 | - description: | 157 | command: | 158 | screenshot: 159 | - ./screenshot00.png 160 | - description: | 161 | command: | 162 | screenshot: 163 | - ./screenshot00.png 164 | - description: | 165 | command: | 166 | screenshot: 167 | - ./screenshot00.png 168 | - description: | 169 | command: | 170 | screenshot: 171 | - ./screenshot00.png 172 | - description: | 173 | command: | 174 | screenshot: 175 | - ./screenshot00.png 176 | - description: | 177 | command: | 178 | screenshot: 179 | - ./screenshot00.png 180 | vuln: 181 | - cve: 182 | edb: 183 | links: 184 | - 185 | loot: 186 | hashes: 187 | - 188 | credentials: 189 | ftp: 190 | - 191 | ssh: 192 | - 193 | wordpress: 194 | - 195 | flags: 196 | - 197 | learning: 198 | - 199 | -------------------------------------------------------------------------------- /template.writeup.md: -------------------------------------------------------------------------------- 1 | --- 2 | lang: "en" 3 | classoption: oneside 4 | code-block-font-size: \scriptsize 5 | geometry: "a4paper" 6 | geometry: "margin=2cm" 7 | header-includes: 8 | - \usepackage{float} 9 | - \floatplacement{figure}{H} 10 | - \usepackage{xcolor} 11 | - \hypersetup{breaklinks=true, 12 | bookmarks=true, 13 | pdftitle="{{ writeup.metadata.name|replace(':', '')|replace('#', '') }}", 14 | pdfauthor="svachal (@7h3rAm)", 15 | pdfsubject='Writeup for {{ writeup.metadata.infra }} VM {{ writeup.metadata.name|replace(':', '')|replace('#', '') }}', 16 | pdfkeywords="{{ writeup.metadata.categories|join(' ') }}", 17 | colorlinks=true, 18 | linkcolor=cyan, 19 | urlcolor=blue} 20 | - \usepackage{fvextra} 21 | - \DefineVerbatimEnvironment{Highlighting}{Verbatim}{breaklines,breakanywhere=true,commandchars=\\\{\}} 22 | - \usepackage{mathtools} 23 | --- 24 | 25 | # [[{{ writeup.metadata.infra }}] {{ writeup.metadata.name }}]({{writeup.metadata.url}}) 26 | 27 | **Date**: {{ writeup.metadata.datetime|datetimefilter("%d/%b/%Y") }} 28 | **Categories**: {{ writeup.metadata.categories|ghsearchlinks("https://github.com/7h3rAm/writeups") }} 29 | **Tags**: {{ writeup.metadata.tags|ghsearchlinks("https://github.com/7h3rAm/writeups") }} 30 | {% if writeup.metadata.infocard %} 31 | {% endif %} 32 | 33 | ## Overview 34 | {% if writeup.overview %} 35 | This is a writeup for {{ writeup.metadata.infra }} VM [{{ writeup.metadata.name }}]({{ writeup.metadata.url }}). Here are stats for this machine from [machinescli](https://github.com/7h3rAm/machinescli): 36 | 37 | ![writeup.overview.machinescli](./machinescli.png) 38 | 39 | ### Killchain 40 | Here's the killchain (`enumeration` → `exploitation` → `privilege escalation`) for this machine: 41 | 42 | ![writeup.overview.killchain](./killchain.png) 43 | 44 | {% if writeup.overview.ttps and writeup.overview.ttps|length %} 45 | 46 | ### TTPs 47 | {% for portmap in writeup.overview.ttps %} 48 | {{ loop.index }}\. `{{ portmap }}`: {{ writeup.overview.ttps[portmap].split(" ")|anchorformatttps("https://github.com/7h3rAm/writeups") }} 49 | {% endfor %} 50 | 51 | {% endif %} 52 | {% endif %} 53 | 54 | \newpage 55 | {% if writeup.enumeration %}## Phase #1: Enumeration 56 | {% if writeup.enumeration.steps %} 57 | {% for step in writeup.enumeration.steps %} 58 | {% set step_loop = loop %} 59 | {{ loop.index }}\. {{ step.description|trim }} 60 | {% if step.command %} 61 | ``` {.python .numberLines} 62 | {{ step.command }} 63 | ``` 64 | {% endif %} 65 | {% if step.screenshot and step.screenshot|length and step.screenshot[0] %} 66 | {% for screenshot in step.screenshot %} 67 | 68 | ![writeup.enumeration.steps.{{ step_loop.index }}.{{ loop.index }}]({{ screenshot }}) 69 | {% endfor %} 70 | {% endif %} 71 | 72 | {% endfor %} 73 | {% endif %} 74 | 75 | {% if writeup.enumeration.findings %}### Findings 76 | {% if writeup.enumeration.findings.openports %}#### Open Ports 77 | ``` {.python .numberLines} 78 | {{ writeup.enumeration.findings.openports|join("\n") }} 79 | {% endif %} 80 | ``` 81 | {% if writeup.enumeration.findings.files %}#### Files 82 | ``` {.python .numberLines} 83 | {{ writeup.enumeration.findings.files|join("\n") }} 84 | ``` 85 | {% endif %} 86 | {% if writeup.enumeration.findings.users %}#### Users 87 | ``` {.python .numberLines} 88 | {% for service in writeup.enumeration.findings.users %} 89 | {{ service }}: {{ writeup.enumeration.findings.users[service]|join(", ") }} 90 | {% endfor %} 91 | ``` 92 | {% endif %} 93 | {% endif %} 94 | {% endif %} 95 | 96 | \newpage 97 | {% if writeup.exploitation %}## Phase #2: Exploitation 98 | {% if writeup.exploitation.steps %} 99 | {% for step in writeup.exploitation.steps %} 100 | {% set step_loop = loop %} 101 | {{ loop.index }}\. {{ step.description|trim }} 102 | {% if step.command %} 103 | ``` {.python .numberLines} 104 | {{ step.command }} 105 | ``` 106 | {% endif %} 107 | {% if step.screenshot and step.screenshot|length and step.screenshot[0] %} 108 | {% for screenshot in step.screenshot %} 109 | 110 | ![writeup.exploitation.steps.{{ step_loop.index }}.{{ loop.index }}]({{ screenshot }}) 111 | {% endfor %} 112 | {% endif %} 113 | 114 | {% endfor %} 115 | {% endif %} 116 | {% endif %} 117 | 118 | ## Phase #2.5: Post Exploitation 119 | ``` {.python .numberLines} 120 | {% if writeup.postexploit.id %} 121 | {{ writeup.postexploit.user }}@{{ writeup.postexploit.hostname }}> id 122 | {{ writeup.postexploit.id|trim }} 123 | {{ writeup.postexploit.user }}@{{ writeup.postexploit.hostname }}> 124 | {% endif %} 125 | {% if writeup.postexploit.uname %} 126 | {{ writeup.postexploit.user }}@{{ writeup.postexploit.hostname }}> uname 127 | {{ writeup.postexploit.uname|trim }} 128 | {{ writeup.postexploit.user }}@{{ writeup.postexploit.hostname }}> 129 | {% endif %} 130 | {% if writeup.postexploit.ifconfig %} 131 | {{ writeup.postexploit.user }}@{{ writeup.postexploit.hostname }}> ifconfig 132 | {{ writeup.postexploit.ifconfig|trim }} 133 | {{ writeup.postexploit.user }}@{{ writeup.postexploit.hostname }}> 134 | {% endif %} 135 | {% if writeup.postexploit.users %} 136 | {{ writeup.postexploit.user }}@{{ writeup.postexploit.hostname }}> users 137 | {{ writeup.postexploit.users|join("\n") }} 138 | {% endif %} 139 | ``` 140 | 141 | \newpage 142 | {% if writeup.privesc %}## Phase #3: Privilege Escalation 143 | {% if writeup.privesc.steps %} 144 | {% for step in writeup.privesc.steps %} 145 | {% set step_loop = loop %} 146 | {{ loop.index }}\. {{ step.description|trim }} 147 | {% if step.command %} 148 | ``` {.python .numberLines} 149 | {{ step.command }} 150 | ``` 151 | {% endif %} 152 | {% if step.screenshot and step.screenshot|length and step.screenshot[0] %} 153 | {% for screenshot in step.screenshot %} 154 | 155 | ![writeup.privesc.steps.{{ step_loop.index }}.{{ loop.index }}]({{ screenshot }}) 156 | {% endfor %} 157 | {% endif %} 158 | 159 | {% endfor %} 160 | {% endif %} 161 | {% endif %} 162 | 163 | \newpage 164 | {% if writeup.learning and writeup.learning|length and writeup.learning[0] %}## Learning/Recommendation 165 | {% for learning in writeup.learning %} 166 | * {{ learning }} 167 | {% endfor %} 168 | {% endif %} 169 | 170 | {% if writeup.loot %}## Loot 171 | {% if writeup.loot.hashes and writeup.loot.hashes|length > 0 and writeup.loot.hashes[0] != None %}### Hashes 172 | ``` {.python .numberLines} 173 | {{ writeup.loot.hashes|obfuscate|join("\n") }} 174 | ``` 175 | {% endif %} 176 | {% if writeup.loot.credentials and writeup.loot.credentials|length > 0 and writeup.loot.credentials[0] != None %}### Credentials 177 | ``` {.python .numberLines} 178 | {% for service in writeup.loot.credentials %} 179 | {{ service }}: {{ writeup.loot.credentials[service]|obfuscate|join(", ") }} 180 | {% endfor %} 181 | ``` 182 | {% endif %} 183 | {% if writeup.loot.flags and writeup.loot.flags|length > 0 and writeup.loot.flags[0] != None %}### Flags 184 | ``` {.python .numberLines} 185 | {{ writeup.loot.flags|obfuscate|join("\n") }} 186 | ``` 187 | {% endif %} 188 | {% endif %} 189 | 190 | {% if writeup.metadata.url or writeup.metadata.references %}## References 191 | {% if writeup.metadata.url %} 192 | [+] <{{ writeup.metadata.url }}> 193 | {% endif %} 194 | {% for reference in writeup.metadata.references %} 195 | [+] <{{ reference }}> 196 | {% endfor %} 197 | {% endif %} 198 | -------------------------------------------------------------------------------- /template.readme.md: -------------------------------------------------------------------------------- 1 | # 📖 ReadMe 2 | 3 | [![License: CC BY-SA 4.0](https://raw.githubusercontent.com/7h3rAm/7h3rAm.github.io/master/static/files/ccbysa4.svg)](https://creativecommons.org/licenses/by-sa/4.0/) 4 | 5 | 6 | ## 🔖 Contents 7 | - ☀️ [Methodology](#methodology) 8 | * ⚙️ [Phase 0: Recon](#mrecon) 9 | * ⚙️ [Phase 1: Enumerate](#menumerate) 10 | * ⚙️ [Phase 2: Exploit](#mexploit) 11 | * ⚙️ [Phase 3: PrivEsc](#mprivesc) 12 | 13 | - ☀️ [Stats](#stats) 14 | * 📊 [Counts](#counts) 15 | * 📊 [Top Categories](#topcategories) 16 | * 📊 [Top Ports/Protocols/Services](#topportsprotocolsservices) 17 | * 📊 [Top TTPs](#topttps) 18 | 19 | - ⚡ [Mapping](#mapping) 20 | 21 | - 💥 [Machines](#machines) 22 | 23 | - ☢️ [TTPs](#ttps) 24 | * ⚙️ [Enumerate](#enumerate) 25 | * ⚙️ [Exploit](#exploit) 26 | * ⚙️ [PrivEsc](#privesc) 27 | 28 | - ⚡ [Tips](#tips) 29 | 30 | - 💥 [Tools](#tools) 31 | 32 | - 🔥 [Loot](#loot) 33 | * 🔑 [Credentials](#credentials) 34 | * 🔑 [Hashes](#hashes) 35 | 36 | 37 | 38 | ## ☀️ Methodology [↟](#contents) 39 | 40 | ### ⚙️ Phase #0: Recon [🡑](#methodology) 41 | **Goal**: {{ summary.methodology.recon.goal|e }} 42 | **Process**: 43 | {% for item in summary.methodology.recon.process %} 44 | * {{ item|e }} 45 | {% endfor %} 46 | 47 | 48 | ### ⚙️ Phase #1: Enumerate [🡑](#methodology) 49 | **Goal**: {{ summary.methodology.enumerate.goal|e }} 50 | **Process**: 51 | {% for item in summary.methodology.enumerate.process %} 52 | * {{ item|e }} 53 | {% endfor %} 54 | 55 | 56 | ### ⚙️ Phase #2: Exploit [🡑](#methodology) 57 | **Goal**: {{ summary.methodology.exploit.goal|e }} 58 | **Process**: 59 | {% for item in summary.methodology.exploit.process %} 60 | * {{ item|e }} 61 | {% endfor %} 62 | 63 | 64 | ### ⚙️ Phase #3: PrivEsc [🡑](#methodology) 65 | **Goal**: {{ summary.methodology.privesc.goal|e }} 66 | **Process**: 67 | {% for item in summary.methodology.privesc.process %} 68 | * {{ item|e }} 69 | {% endfor %} 70 | 71 | 72 | 73 | ## ☀️ Stats [↟](#contents) 74 | ### 📊 Counts [🡑](#stats) 75 | {{ summary.stats.counts }} 76 | 77 | 78 | ### 📊 Top Categories [🡑](#stats) 79 | 80 | 81 | 82 | ### 📊 Top Ports/Protocols/Services [🡑](#stats) 83 | 84 | 85 | --- 86 | 87 | 88 | --- 89 | 90 | 91 | 92 | ### 📊 Top TTPs [🡑](#stats) 93 | 94 | 95 | --- 96 | 97 | 98 | --- 99 | 100 | 101 | 102 | 103 | ## ⚡ Mapping [↟](#contents) 104 | | # | Port | Service | TTPs | TTPs - ITW | 105 | |---|------|-----------|------|------------| 106 | {% for item in summary.ttpsitw|customsort %} 107 | | {{ loop.index }}. | `{{ item }}/{{ summary.ttpsitw[item].l4 }}` | {{ summary.ttpsitw[item].protokeys|monojoin }} | {{ summary.ttpsitw[item].ttps|anchorformat("https://github.com/7h3rAm/writeups") }} | {{ summary.ttpsitw[item].ttpsitw|anchorformat("https://github.com/7h3rAm/writeups") }} | 108 | {% endfor %} 109 | 110 | 111 | 112 | ## 💥 Machines [↟](#contents) 113 | {{ summary.stats.owned }} 114 | 115 | 116 | 117 | ## ☢️ TTPs [↟](#contents) 118 | 119 | ### ⚙️ Enumerate [🡑](#ttps) 120 | {% for item in summary.techniques.enumerate %} 121 | {% set outerloop = loop %} 122 | 123 | #### {{ item }} [⇡](#enumerate) 124 | {% if summary.techniques.enumerate[item].description %} 125 | {{ summary.techniques.enumerate[item].description }} 126 | {% endif %} 127 | {% if summary.techniques.enumerate[item].cli %} 128 | ```shell 129 | {{ summary.techniques.enumerate[item].cli }} 130 | ``` 131 | {% endif %} 132 | {% if summary.techniques.enumerate[item].writeups|length > 0 %} 133 | | # | Name | Infra | Killchain | TTPs | 134 | |---|------|-------|-----------|------| 135 | {% for entry in summary.techniques.enumerate[item].writeups|sort(attribute="datetime", reverse=True) %} 136 | | {{ loop.index }}. | [{{ entry.name }}]({{ entry.writeup }}) | [{{ entry.verbose_id }}]({{ entry.url }}) | {{ entry.overview }} | {{ entry.tags|anchorformat("https://github.com/7h3rAm/writeups") }} | 137 | {% endfor %} 138 | {% endif %} 139 | {% for reference in summary.techniques.enumerate[item].references %} 140 | {% if reference %} 141 | [+] {{ reference }} 142 | {% endif %} 143 | {% endfor %} 144 | --- 145 | {% endfor %} 146 | 147 | 148 | ### ⚙️ Exploit [🡑](#ttps) 149 | {% for item in summary.techniques.exploit %} 150 | {% set outerloop = loop %} 151 | 152 | #### {{ item }} [⇡](#exploit) 153 | {% if summary.techniques.exploit[item].description %} 154 | {{ summary.techniques.exploit[item].description }} 155 | {% endif %} 156 | {% if summary.techniques.exploit[item].cli %} 157 | ```shell 158 | {{ summary.techniques.exploit[item].cli }} 159 | ``` 160 | {% endif %} 161 | {% if summary.techniques.exploit[item].writeups|length > 0 %} 162 | | # | Name | Infra | Killchain | TTPs | 163 | |---|------|-------|-----------|------| 164 | {% for entry in summary.techniques.exploit[item].writeups|sort(attribute="datetime", reverse=True) %} 165 | | {{ loop.index }}. | [{{ entry.name }}]({{ entry.writeup }}) | [{{ entry.verbose_id }}]({{ entry.url }}) | {{ entry.overview }} | {{ entry.tags|anchorformat("https://github.com/7h3rAm/writeups") }} | 166 | {% endfor %} 167 | {% endif %} 168 | {% for reference in summary.techniques.exploit[item].references %} 169 | {% if reference %} 170 | [+] {{ reference }} 171 | {% endif %} 172 | {% endfor %} 173 | --- 174 | {% endfor %} 175 | 176 | 177 | ### ⚙️ PrivEsc [🡑](#ttps) 178 | {% for item in summary.techniques.privesc %} 179 | {% set outerloop = loop %} 180 | 181 | #### {{ item }} [⇡](#privesc) 182 | {% if summary.techniques.privesc[item].description %} 183 | {{ summary.techniques.privesc[item].description }} 184 | {% endif %} 185 | {% if summary.techniques.privesc[item].cli %} 186 | ```shell 187 | {{ summary.techniques.privesc[item].cli }} 188 | ``` 189 | {% endif %} 190 | {% if summary.techniques.privesc[item].writeups|length > 0 %} 191 | | # | Name | Infra | Killchain | TTPs | 192 | |---|------|-------|-----------|------| 193 | {% for entry in summary.techniques.privesc[item].writeups|sort(attribute="datetime", reverse=True) %} 194 | | {{ loop.index }}. | [{{ entry.name }}]({{ entry.writeup }}) | [{{ entry.verbose_id }}]({{ entry.url }}) | {{ entry.overview }} | {{ entry.tags|anchorformat("https://github.com/7h3rAm/writeups") }} | 195 | {% endfor %} 196 | {% endif %} 197 | {% for reference in summary.techniques.privesc[item].references %} 198 | {% if reference %} 199 | [+] {{ reference }} 200 | {% endif %} 201 | {% endfor %} 202 | --- 203 | {% endfor %} 204 | 205 | 206 | 207 | ## ⚡ Tips [↟](#contents) 208 | {% for entry in summary.tips %} 209 | ### {{ entry.description|trim }} [🡑](#tips) 210 | ``` 211 | {{ entry.cli|trim }} 212 | ``` 213 | {% endfor %} 214 | 215 | 216 | 217 | ## 💥 Tools [↟](#contents) 218 | {% for entry in summary.tools %} 219 | ### {{ entry.description|trim }} [🡑](#tools) 220 | ``` 221 | {{ entry.cli|trim }} 222 | ``` 223 | {% endfor %} 224 | 225 | 226 | 227 | ## 🔥 Loot [↟](#contents) 228 | 229 | ### 🔑 Credentials [🡑](#loot) 230 | | # | Username | Password | Type | 231 | |---|----------|----------|------| 232 | {% for item in summary.loot.credentials|sort(attribute="credtype", reverse=False) %} 233 | | {{ loop.index }}. | {% if item.username %}`{{ item.username }}`{% endif %} | `{{ item.password|obfuscate }}` | `{{ item.credtype }}` | 234 | {% endfor %} 235 | 236 | 237 | ### 🔑 Hashes [🡑](#loot) 238 | | # | Hash | 239 | |---|------| 240 | {% for item in summary.loot.hashes|sort %} 241 | | {{ loop.index }}. | `{{ item }}` | 242 | {% endfor %} 243 | -------------------------------------------------------------------------------- /yml2dot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import print_function 4 | from pprint import pprint 5 | import subprocess 6 | import hashlib 7 | import secrets 8 | import yaml 9 | import sys 10 | import re 11 | import os 12 | 13 | """ 14 | https://stackoverflow.design/product/base/colors/ 15 | 16 | rgb(244, 248, 251) #f4f8fb 17 | rgb(225, 236, 244) #e1ecf4 18 | rgb(209, 229, 241) #d1e5f1 19 | rgb(0, 119, 204) #0077cc 20 | rgb(91, 141, 177) #5b8db1 21 | 22 | rgb(253, 247, 227) #fdf7e3 23 | rgb(251, 242, 212) #fbf2d4 24 | rgb(241, 229, 188) #f1e5bc 25 | rgb(206, 165, 27) #cea51b 26 | 27 | rgb(238, 248, 241) #eef8f1 28 | rgb(220, 240, 226) #dcf0e2 29 | rgb(202, 232, 212) #cae8d4 30 | rgb(61, 143, 88) #3d8f58 31 | 32 | rgb(253, 243, 244) #fdf3f4 33 | rgb(249, 211, 215) #f9d3d7 34 | rgb(244, 180, 187) #f4b4bb 35 | rgb(192, 45, 46) #c02d2e 36 | """ 37 | 38 | 39 | class YML2DOT: 40 | def __init__(self, rootnode="!@#$", fontsize="medium", addrootnode=True, rankdirlr=False, randomnodecolor=True, savehtml=False): 41 | self.nodesdict = {} 42 | self.nodes = [] 43 | self.edges = [] 44 | self.config = { 45 | "rootnode": rootnode, 46 | "fontsize": 18 if fontsize.strip().lower() == "large" else 16 if fontsize.strip().lower() == "medium" else 12, 47 | "rankdirlr": rankdirlr, 48 | "addrootnode": addrootnode, 49 | "randomnodecolor": randomnodecolor, 50 | "savehtml": savehtml, 51 | "ignorekeys": ["__metadata__"], 52 | "writeupyml": False, 53 | 54 | "cluster0": { 55 | "title": "", 56 | "colornode": "#e1ecf4", 57 | "colorborder": "#0077cc", 58 | }, 59 | "cluster1": { 60 | "title": "Phase #1:Enumeration", 61 | "colornode": "#dcf0e2", 62 | "colorborder": "#3d8f58", 63 | }, 64 | "cluster2": { 65 | "title": "Phase #2:Exploitation", 66 | "colornode": "#fbf2d4", 67 | "colorborder": "#cea51b", 68 | }, 69 | "cluster3": { 70 | "title": "Phase #3:Privilege Escalation", 71 | "colornode": "#f9d3d7", 72 | "colorborder": "#c02d2e", 73 | }, 74 | 75 | "colorpallete": ["#cceabb", "#ffe0ac", "#ffacb7", "#b5cbcc", "#becddb", "#c2d6e1", "#c5d0cc", "#cddeef", "#dad4d5", "#dce1e4", "#dee9e7", "#dfdada", "#e3f2f7", "#ece1db", "#eff0ea", "#f3dbbe", "#fac3ac", "#ffcaaf"], 76 | "colorborder": "#665957", 77 | "colorlink": "#85285d", 78 | "colorbg": "#ffffff", 79 | "coloredge": "#665957", 80 | "colornode": "#FFFFFF", 81 | "colorwriteuproot": "", 82 | } 83 | 84 | def md5(self, data): 85 | return hashlib.md5(data.encode('utf-8')).hexdigest() 86 | 87 | def get_edges(self, treedict, parent=None): 88 | if not parent and self.config["addrootnode"]: 89 | parent = self.config["rootnode"] 90 | if isinstance(treedict, dict): 91 | for key in treedict.keys(): 92 | if key in self.config["ignorekeys"]: 93 | continue 94 | self.update_nodes_edges(parent, key) 95 | self.get_edges(treedict[key], parent=key) 96 | elif isinstance(treedict, list): 97 | for item in treedict: 98 | self.get_edges(item, parent=parent) 99 | elif isinstance(treedict, str): 100 | self.update_nodes_edges(parent, treedict) 101 | 102 | def update_nodes_edges(self, parent, child, indent=0, borderless=False): 103 | def update_nodes(node, spaces, indent, borderless): 104 | if node not in self.nodesdict: 105 | label, href, tooltip, image = node, None, node, None 106 | match = re.match(r'^{{\s*(.*)\s*}}$', node, re.M) 107 | if match: 108 | tooltip = match.groups()[0] 109 | label = tooltip 110 | match = re.match(r'^\[\s*(.*)\s*\]\s*\(\s*(.*)\s*\)$', node, re.M) 111 | if match: 112 | label, href = match.groups() 113 | tooltip = label 114 | match = re.match(r'^\[\s*(.*)\s*\]\s*\(\s*(.*)\s*\)\s*{{\s*(.*)\s*}}$', node, re.M) 115 | if match: 116 | label, href, tooltip = match.groups() 117 | 118 | match = re.match(r'^!\[\s*(.*)\s*\]\s*\(\s*(.*)\s*\)$', node, re.M) 119 | if match: 120 | label, image = match.groups() 121 | tooltip = label 122 | match = re.match(r'^!\[\s*(.*)\s*\]\s*\(\s*(.*)\s*\)\s*{{\s*(.*)\s*}}$', node, re.M) 123 | if match: 124 | label, image, tooltip = match.groups() 125 | 126 | nodecolor = secrets.choice(self.config["colorpallete"]) if self.config["randomnodecolor"] else self.config["colornode"] 127 | self.config["writeupyml"] = False 128 | bordercolor = self.config["colorborder"] 129 | if label.startswith("_ "): 130 | self.config["writeupyml"] = True 131 | self.config["cluster0"]["title"], label = label.split("_ ")[1].split("/") 132 | tooltip = self.config["cluster0"]["title"] 133 | nodecolor = self.config["cluster0"]["colornode"] 134 | bordercolor = self.config["cluster0"]["colorborder"] 135 | elif label.startswith(". "): 136 | self.config["writeupyml"] = True 137 | label = label.split(". ")[1] 138 | tooltip = self.config["cluster1"]["title"] 139 | nodecolor = self.config["cluster1"]["colornode"] 140 | bordercolor = self.config["cluster1"]["colorborder"] 141 | elif label.startswith(".. "): 142 | self.config["writeupyml"] = True 143 | label = label.split(".. ")[1] 144 | tooltip = self.config["cluster2"]["title"] 145 | nodecolor = self.config["cluster2"]["colornode"] 146 | bordercolor = self.config["cluster2"]["colorborder"] 147 | elif label.startswith("... "): 148 | self.config["writeupyml"] = True 149 | label = label.split("... ")[1] 150 | tooltip = self.config["cluster3"]["title"] 151 | nodecolor = self.config["cluster3"]["colornode"] 152 | bordercolor = self.config["cluster3"]["colorborder"] 153 | 154 | self.nodesdict[node] = { 155 | "nodeid": len(self.nodesdict), 156 | "label": label, 157 | "href": href, 158 | "image": image, 159 | "tooltip": tooltip, 160 | "color": nodecolor, 161 | "colorborder": bordercolor, 162 | } 163 | self.nodesdict[node]["colorborder"] = self.nodesdict[node]["color"] if borderless else self.nodesdict[node]["colorborder"] 164 | 165 | attribs = [] 166 | 167 | if self.nodesdict[node]["image"]: 168 | attribs.append("label=\"\"") 169 | attribs.append("image=\"%s\"" % (self.nodesdict[node]["image"])) 170 | else: 171 | attribs.append("label=\"%s\"" % (self.nodesdict[node]["label"])) 172 | 173 | if self.nodesdict[node]["href"]: 174 | attribs.append("href=\"%s\"" % (self.nodesdict[node]["href"])) 175 | attribs.append("fontcolor=\"%s\"" % (self.config["colorlink"])) 176 | 177 | attribs.append("color=\"%s\"" % (self.nodesdict[node]["colorborder"])) 178 | attribs.append("fillcolor=\"%s\"" % (self.nodesdict[node]["color"])) 179 | attribs.append("tooltip=\"%s\"" % (self.nodesdict[node]["tooltip"])) 180 | self.nodes.append("%s%d[%s];" % (spaces, self.nodesdict[node]["nodeid"], " ".join(attribs))) 181 | 182 | spaces = " " * indent 183 | 184 | if parent: 185 | parent = parent.strip() 186 | update_nodes(parent, spaces, indent, borderless) 187 | if child: 188 | child = child.strip() 189 | update_nodes(child, spaces, indent, borderless) 190 | if parent and child: 191 | self.edges.append("%s%d -> %d [color=\"%s\"];" % (spaces, self.nodesdict[parent]["nodeid"], self.nodesdict[child]["nodeid"], self.config["coloredge"])) 192 | 193 | def process(self, ymldata, dotfile): 194 | ymlfile = dotfile.replace("dot", "yml") 195 | 196 | self.nodesdict = {} 197 | self.nodes = [] 198 | self.edges = [] 199 | 200 | self.get_edges(ymldata) 201 | self.nodes, self.edges = sorted(list(set(self.nodes))), sorted(list(set(self.edges))) 202 | 203 | dotgraph = [] 204 | dotgraph.append("digraph G {") 205 | dotgraph.append(" rankdir=LR;" if self.config["rankdirlr"] else " #rankdir=LR;") 206 | dotgraph.append(" nodesdictep=1.0; splines=\"ortho\"; K=0.6; overlap=scale; fixedsize=true; resolution=72; bgcolor=\"%s\"; outputorder=\"edgesfirst\";" % (self.config["colorbg"])) 207 | dotgraph.append(" node [fontname=\"courier\" fontsize=%s shape=box width=0.25 fillcolor=\"white\" style=\"filled,solid\"];" % (self.config["fontsize"])) 208 | dotgraph.append(" edge [style=solid color=\"%s\" penwidth=0.75 arrowhead=vee arrowsize=0.75 ];" % (self.config["coloredge"])) 209 | dotgraph.append("") 210 | dotgraph.extend([" %s" % (x) for x in self.nodes]) 211 | dotgraph.append("") 212 | dotgraph.append(" subgraph cluster_0 {") 213 | dotgraph.append(" node [style=\"filled,solid\"];") 214 | dotgraph.append(" label = \"%s\";" % (self.config["cluster0"]["title"] if self.config["writeupyml"] else os.path.basename(dotfile))) 215 | dotgraph.append(" color = \"%s\";" % (self.config["colorborder"])) 216 | dotgraph.extend([" %s" % (x) for x in self.edges]) 217 | dotgraph.append(" }") 218 | dotgraph.append("}") 219 | dotgraph.append("") 220 | with open(dotfile, "w") as fp: 221 | fp.write("\n".join(dotgraph)) 222 | 223 | if self.config["savehtml"]: 224 | d3graph = [] 225 | d3graph.append("") 226 | d3graph.append("") 227 | d3graph.append("") 228 | d3graph.append("") 229 | d3graph.append("") 230 | d3graph.append("") 231 | d3graph.append("
") 232 | d3graph.append("") 237 | htmlfile = "%s.html" % (".".join(dotfile.split(".")[:-1])) 238 | with open(htmlfile, "w") as fp: 239 | fp.write("\n".join(d3graph)) 240 | 241 | pngfile = "%s.png" % (".".join(dotfile.split(".")[:-1])) 242 | subprocess.call("/usr/bin/dot -Tpng %s -o %s" % (dotfile, pngfile), shell=True) 243 | 244 | return dotgraph 245 | 246 | 247 | if __name__ == "__main__": 248 | if len(sys.argv) != 2: 249 | print("USAGE: %s " % (sys.argv[0])) 250 | sys.exit(1) 251 | 252 | if not os.path.exists(sys.argv[-1]): 253 | print("no such file: %s" % (sys.argv[-1])) 254 | sys.exit(2) 255 | 256 | infilename = sys.argv[-1] 257 | outfileprefix = ".".join(infilename.split(".")[:-1]) 258 | 259 | with open(infilename) as fp: 260 | ymldata = yaml.safe_load(fp) 261 | 262 | y2d = YML2DOT(rootnode="!@#$", fontsize="medium", addrootnode=False, rankdirlr=False, randomnodecolor=True, savehtml=True) 263 | y2d.process(ymldata, outfileprefix) 264 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import glob 4 | import json 5 | import yaml 6 | import errno 7 | import codecs 8 | import fnmatch 9 | import datetime 10 | import urllib.request 11 | 12 | import requests 13 | import sparkline 14 | import prettytable 15 | from bs4 import BeautifulSoup 16 | import matplotlib.pyplot as plt 17 | 18 | 19 | def highlight(text, color="black", bold=False): 20 | resetcode = "\x1b[0m" 21 | color = color.lower().strip() 22 | if color == "black": 23 | colorcode = "\x1b[0;30m" if not bold else "\x1b[1;30m" 24 | elif color == "white": 25 | colorcode = "\x1b[0;37m" if not bold else "\x1b[1;37m" 26 | elif color == "red": 27 | colorcode = "\x1b[0;31m" if not bold else "\x1b[1;31m" 28 | elif color == "green": 29 | colorcode = "\x1b[0;32m" if not bold else "\x1b[1;32m" 30 | elif color == "yellow": 31 | colorcode = "\x1b[0;33m" if not bold else "\x1b[1;33m" 32 | elif color == "blue": 33 | colorcode = "\x1b[0;34m" if not bold else "\x1b[1;34m" 34 | elif color == "magenta": 35 | colorcode = "\x1b[0;35m" if not bold else "\x1b[1;35m" 36 | elif color == "cyan": 37 | colorcode = "\x1b[0;36m" if not bold else "\x1b[1;36m" 38 | else: 39 | colorcode = "\x1b[0;30m" if not bold else "\x1b[1;30m" 40 | return "%s%s%s" % (colorcode, text, resetcode) 41 | 42 | def black(text): 43 | return highlight(text, color="black", bold=False) 44 | 45 | def black_bold(text): 46 | return highlight(text, color="black", bold=True) 47 | 48 | def white(text): 49 | return highlight(text, color="white", bold=False) 50 | 51 | def white_bold(text): 52 | return highlight(text, color="white", bold=True) 53 | 54 | def red(text): 55 | return highlight(text, color="red", bold=False) 56 | 57 | def red_bold(text): 58 | return highlight(text, color="red", bold=True) 59 | 60 | def green(text): 61 | return highlight(text, color="green", bold=False) 62 | 63 | def green_bold(text): 64 | return highlight(text, color="green", bold=True) 65 | 66 | def yellow(text): 67 | return highlight(text, color="yellow", bold=False) 68 | 69 | def yellow_bold(text): 70 | return highlight(text, color="yellow", bold=True) 71 | 72 | def blue(text): 73 | return highlight(text, color="blue", bold=False) 74 | 75 | def blue_bold(text): 76 | return highlight(text, color="blue", bold=True) 77 | 78 | def magenta(text): 79 | return highlight(text, color="magenta", bold=False) 80 | 81 | def magenta_bold(text): 82 | return highlight(text, color="magenta", bold=True) 83 | 84 | def cyan(text): 85 | return highlight(text, color="cyan", bold=False) 86 | 87 | def cyan_bold(text): 88 | return highlight(text, color="cyan", bold=True) 89 | 90 | def debug(text): 91 | print("%s %s" % (blue_bold("[*]"), text)) 92 | 93 | def info(text): 94 | print("%s %s" % (green_bold("[+]"), text)) 95 | 96 | def warn(text): 97 | print("%s %s" % (yellow_bold("[!]"), text)) 98 | 99 | def error(text): 100 | print("%s %s" % (red_bold("[-]"), text)) 101 | 102 | def expand_env(var="$HOME"): 103 | return os.environ[var.replace("$", "")] 104 | 105 | def trim(text, maxq=40): 106 | return "%s..." % (text[:maxq]) if len(text) > maxq else text 107 | 108 | def mkdirp(path): 109 | try: 110 | os.makedirs(path) 111 | except OSError as exc: 112 | if exc.errno == errno.EEXIST and os.path.isdir(path): 113 | pass 114 | else: 115 | raise 116 | 117 | def search_files(dirpath="./", regex="*"): 118 | matches = [] 119 | for root, dirnames, filenames in os.walk(dirpath): 120 | for filename in fnmatch.filter(filenames, regex): 121 | resultfile = os.path.join(root, filename) 122 | if os.path.exists(resultfile): 123 | matches.append(resultfile) 124 | fm = filter(lambda item: '/__pycache__' not in item and '/results' not in item and '/.git' not in item and '/summary.yml' not in item and '/meta.yml' not in item and '/ttps.yml' not in item and '/test.ttp.yml' not in item, matches) 125 | return list(set(fm)) 126 | 127 | def search_files_all(dirpath): 128 | return search_files(dirpath, regex="*") 129 | 130 | def search_files_yml(dirpath): 131 | return search_files(dirpath, regex="*.yml") 132 | 133 | def search_files_md(dirpath): 134 | return search_files(dirpath, regex="*.md") 135 | 136 | def download_json(url): 137 | with urllib.request.urlopen(url) as url: 138 | return json.loads(url.read().decode()) 139 | 140 | def load_json(filename): 141 | with open(filename) as fp: 142 | return json.load(fp) 143 | 144 | def save_json(datadict, filename): 145 | with open(filename, "w", encoding="utf-8") as fp: 146 | json.dump(datadict, fp, ensure_ascii=False, indent=2, sort_keys=True) 147 | 148 | def load_file(filename): 149 | lines = [] 150 | with open(filename) as fp: 151 | lines = sorted(list(set(list(list(filter(None, fp.read().split("\n"))))))) 152 | return lines 153 | 154 | def save_file(datalist, filename): 155 | with open(filename, "w") as fp: 156 | fp.write("\n".join(sorted(list(set(list(list(filter(None, datalist)))))))) 157 | fp.write("\n") 158 | 159 | def load_yaml(filename): 160 | return yaml.safe_load(open(filename)) 161 | 162 | def save_yaml(datayml, filename): 163 | with open(filename, "w") as fp: 164 | yaml.dump(datayml, fp, default_flow_style=True) 165 | 166 | def dict2yaml(datadict): 167 | return yaml.safe_dump(yaml.load(json.dumps(datadict), Loader=yaml.FullLoader), default_flow_style=False) 168 | 169 | def file_open(filename): 170 | if filename and filename != "": 171 | with codecs.open(filename, mode="r", encoding="utf-8") as fo: 172 | return fo.read() 173 | 174 | def file_save(filename, data, mode="w"): 175 | if filename and filename != "": 176 | if "/" in filename: 177 | mkdirp(os.path.dirname(filename)) 178 | try: 179 | with codecs.open(filename, mode, encoding="utf-8") as fo: 180 | fo.write(data) 181 | except Exception as ex: 182 | with open(filename, mode) as fo: 183 | try: 184 | fo.write(data) 185 | except: 186 | fo.write(data.encode('utf-16', 'surrogatepass').decode('utf-16')) 187 | 188 | def download(url, filename): 189 | res = requests.get(url) 190 | if res.status_code == 200: 191 | open(filename, "wb").write(res.content) 192 | 193 | def get_http_res(url, headers={}, requoteuri=False): 194 | if requoteuri: 195 | return requests.get(cleanup_url(requests.utils.requote_uri(url)), headers=headers) 196 | else: 197 | return requests.get(cleanup_url(url), headers=headers) 198 | 199 | def get_http(url, headers={}): 200 | res = requests.get(cleanup_url(url), headers=headers) 201 | if res.status_code == 200: 202 | return res.json() 203 | else: 204 | return {} 205 | 206 | def post_http(url, data={}, headers={}): 207 | res = requests.post(cleanup_url(url), data=json.dumps(data), headers=headers) 208 | if res.status_code == 200: 209 | return res.json() 210 | else: 211 | return {} 212 | 213 | def strip_html(data): 214 | return re.sub("\s+", " ", BeautifulSoup(data, "lxml").text) 215 | 216 | def datetimefilter(datestr, format='%Y/%m/%d %H:%M:%S'): 217 | try: 218 | return datetime.datetime.strptime(str(datestr), '%Y%m%dT%H:%M:%SZ').strftime(format) 219 | except: 220 | return datetime.datetime.strptime(str(datestr), '%Y%m%d').strftime(format) 221 | 222 | def cleanup_url(url): 223 | return url.replace("//", "/").replace(":/", "://") 224 | 225 | def cleanup_name(name): 226 | return re.sub(r"[\W_]", "", name.lower()) 227 | return name.lower().replace(" ", "").replace(":", "").replace("_", "").replace("-", "") 228 | 229 | def ghsearchlinks(items, repourl, delim=", "): 230 | if isinstance(items, str): 231 | return "[`%s`](%s/search?q=%s&unscoped_q=%s)" % (items, repourl, items, items) 232 | else: 233 | return delim.join([ "[%s](%s/search?q=%s&unscoped_q=%s)" % (x, repourl, x, x) for x in items]) 234 | 235 | def anchorformat(items, repourl, delim=", "): 236 | if isinstance(items, str): 237 | if items.startswith("enumerate_") or items.startswith("exploit_") or items.startswith("privesc_"): 238 | return "[`%s`](%s#%s)" % (items, repourl, items) 239 | else: 240 | return ghsearchlinks(items, repourl) 241 | else: 242 | results = [] 243 | for x in items: 244 | if x.startswith("enumerate_") or x.startswith("exploit_") or x.startswith("privesc_"): 245 | results.append("[`%s`](%s#%s)" % (x, repourl, x)) 246 | else: 247 | results.append(ghsearchlinks(x, repourl)) 248 | return delim.join(results) 249 | 250 | def anchorformatttps(items, repourl="https://github.com/7h3rAm/writeups", delim=", "): 251 | if isinstance(items, str): 252 | if items.startswith("enumerate_") or items.startswith("exploit_") or items.startswith("privesc_"): 253 | return "[%s](%s#%s)" % (items, repourl, items) 254 | else: 255 | return ghsearchlinks(items, repourl) 256 | else: 257 | results = [] 258 | for x in items: 259 | if x.startswith("enumerate_") or x.startswith("exploit_") or x.startswith("privesc_"): 260 | results.append("[%s](%s#%s)" % (x, repourl, x)) 261 | else: 262 | results.append(ghsearchlinks(x, repourl)) 263 | return delim.join(results) 264 | 265 | def mdurl(datadict): 266 | results = [] 267 | for item in datadict: 268 | results.append("[%s](%s)" % (item["name"], item["url"])) 269 | return "

".join(results) 270 | 271 | def obfuscate(data, mass=0.81, replchr="."): 272 | # calculate event horizon using the given mass 273 | # use eh to hide remaining data forever 274 | if isinstance(data, str): 275 | eh = int(len(data) * mass) 276 | return "".join([data[:eh], len(data[eh:])*replchr]) 277 | else: 278 | results = [] 279 | for x in data: 280 | eh = int(len(x) * mass) 281 | results.append("".join([x[:eh], len(x[eh:])*replchr])) 282 | return results 283 | 284 | def monojoin(items): 285 | if isinstance(items, str): 286 | return "`%s`" % (items) 287 | else: 288 | results = [] 289 | for x in items: 290 | results.append("`%s`" % (x)) 291 | return "

".join(results) 292 | 293 | def customsort(items): 294 | return [str(y) for y in sorted([int(x) for x in items])] 295 | 296 | def lookahead(iterable): 297 | # https://stackoverflow.com/a/1630350 298 | it = iter(iterable) 299 | last = next(it) 300 | for val in it: 301 | yield last, True 302 | last = val 303 | yield last, False 304 | 305 | def yturl2verboseid(url): 306 | v, t = None, None 307 | for param in url.strip().split("?", 1)[1].split("&"): 308 | if param.startswith("v="): 309 | v = param.split("=")[1] 310 | if param.startswith("t="): 311 | t = param 312 | if v and t: 313 | return "youtu.be/%s?%s" % (v, t) 314 | elif v: 315 | return "youtu.be/%s" % (v) 316 | else: 317 | return url 318 | 319 | def sparkify(difficulty): 320 | return sparkline.sparkify(difficulty) 321 | 322 | def to_color_difficulty(sparkline): 323 | return "".join([green(sparkline[:3]), yellow(sparkline[3:7]), red(sparkline[7:])]) 324 | 325 | def to_emoji(text): 326 | text = str(text) 327 | # https://github.com/ikatyang/emoji-cheat-sheet 328 | if "private" == text.lower(): 329 | return red("") #  330 | elif "public" == text.lower(): 331 | return green("") 332 | elif "oscplike" == text.lower(): 333 | return magenta("") 334 | elif "access_root" == text.lower(): 335 | return red("") 336 | elif "access_user" == text.lower(): 337 | return yellow("") 338 | elif "has_writeup" == text.lower(): 339 | return yellow("") 340 | elif "android" in text.lower(): 341 | return green("") 342 | elif "arm" in text.lower(): 343 | return magenta("") 344 | elif "bsd" in text.lower(): 345 | return red("") 346 | elif "linux" == text.lower(): 347 | return yellow("") 348 | elif "solaris" in text.lower(): 349 | return magenta("") 350 | elif "unix" in text.lower(): 351 | return magenta("") 352 | elif "windows" == text.lower(): 353 | return blue("") 354 | elif "other" in text.lower(): 355 | return magenta("") 356 | elif "difficulty_unknown" == text.lower(): 357 | return "" 358 | elif "startingpoint" == text.lower(): 359 | return white("") 360 | elif "info" == text.lower(): 361 | return white("") 362 | elif "warmpup" == text.lower(): 363 | return green("") 364 | elif "easy" == text.lower(): 365 | return green("") 366 | elif "medium" == text.lower(): 367 | return yellow("") 368 | elif "hard" == text.lower(): 369 | return yellow("") 370 | elif "insane" == text.lower(): 371 | return red("") 372 | else: 373 | return "" 374 | 375 | def to_markdown_table(pt): 376 | _junc = pt.junction_char 377 | if _junc != "|": 378 | pt.junction_char = "|" 379 | markdown = [row for row in pt.get_string().split("\n")[1:-1]] 380 | pt.junction_char = _junc 381 | return "\n".join(markdown) 382 | 383 | def get_table(header, rows, delim="___", aligndict=None, markdown=False, colalign=None, multiline=False): 384 | table = prettytable.PrettyTable() 385 | table.field_names = header 386 | table.align = "c"; table.valign = "m" 387 | for row in rows: 388 | table.add_row(row.split(delim)) 389 | if markdown: 390 | if colalign in ["left", "center", "right"]: 391 | if colalign == "left": 392 | return to_markdown_table(table).replace("|-", "|:") 393 | elif colalign == "center": 394 | return to_markdown_table(table).replace("-|-", ":|:").replace("|-", "|:").replace("-|", ":|") 395 | elif colalign == "right": 396 | return to_markdown_table(table).replace("-|", ":|") 397 | else: 398 | #return table.get_html_string() 399 | return to_markdown_table(table) 400 | else: 401 | if aligndict: 402 | for colheader in aligndict: 403 | table.align[colheader] = aligndict[colheader] 404 | else: 405 | table.align["#"] = "r" 406 | table.align["ID"] = "r" 407 | table.align["Name"] = "l" 408 | table.align["Expires"] = "l" 409 | table.align["Match"] = "l" 410 | table.align["Follow"] = "l" 411 | table.align["Private"] = "c" 412 | table.align["Rating"] = "c" 413 | table.align["Difficulty"] = "c" 414 | table.align["OS"] = "c" 415 | table.align["OSCPlike"] = "c" 416 | table.align["Owned"] = "c" 417 | table.align["Writeup"] = "c" 418 | table.align["TTPs"] = "c" 419 | table.vertical_char = " " 420 | table.horizontal_char = "-" 421 | table.junction_char = " " 422 | table.hrules = prettytable.ALL if multiline else prettytable.FRAME 423 | return "\n%s\n" % (table.get_string()) 424 | 425 | def to_table(header, rows, delim="___", aligndict=None, markdown=False, multiline=False): 426 | print(get_table(header, rows, delim=delim, aligndict=aligndict, markdown=markdown, multiline=multiline)) 427 | 428 | def to_json(data): 429 | print(json.dumps(data, indent=2, sort_keys=True)) 430 | 431 | def to_gsheet(data): 432 | lines = [] 433 | for item in data: 434 | name = "=HYPERLINK(\"%s\",\"%s\")" % (item["url"], item["name"]) 435 | if "htb" in item["infrastructure"] or "hackthebox" in item["infrastructure"]: 436 | infra = "HackTheBox" 437 | elif "vh" in item["infrastructure"] or "vulnhub" in item["infrastructure"]: 438 | infra = "VulnHub" 439 | elif "thm" in item["infrastructure"] or "tryhackme" in item["infrastructure"]: 440 | infra = "TryHackMe" 441 | else: 442 | infra = "Misc" 443 | os = item["os"].title() 444 | points = item["points"] if item["points"] else "" 445 | owned = "Yes" if item["owned_user"] or item["owned_root"] else "No" 446 | lines.append("%s,%s,%s,%s,%s," % (name, infra, os, points, owned)) 447 | print("Name,Infra,OS,Points,Difficulty,Owned,Writeup") 448 | for line in sorted(lines): 449 | print(line) 450 | 451 | def show_machines(data, sort_key="name", jsonify=False, gsheet=False, showttps=False): 452 | if not len(data): 453 | return 454 | elif "success" in data: 455 | return to_json(data) 456 | elif jsonify: 457 | to_json(data) 458 | elif gsheet: 459 | to_gsheet(data) 460 | else: 461 | rows = [] 462 | if data[0].get("expires_at"): 463 | header = ["#", "ID", "Name", "Expires", "Rating", "Difficulty", "OS", "OSCPlike", "Owned", "Writeup", "TTPs"] if showttps else ["#", "ID", "Name", "Expires", "Rating", "Difficulty", "OS", "OSCPlike", "Owned", "Writeup"] 464 | for idx, entry in enumerate(sorted(data, key=lambda k: k[sort_key].lower())): 465 | mid = "%s%s" % (blue("%s#" % (entry["verbose_id"].split("#")[0])), blue_bold("%s" % (entry["verbose_id"].split("#")[1]))) 466 | name = trim(entry["name"], maxq=30) 467 | os = to_emoji(entry["os"]) 468 | difficulty = entry["difficulty"] if entry.get("difficulty") and entry["difficulty"] else "difficulty_unknown" 469 | rating = to_color_difficulty(sparkify(entry["difficulty_ratings"])) if entry.get("difficulty_ratings") else "" 470 | oscplike = "oscplike" if entry.get("oscplike") and entry["oscplike"] else "notoscplike" 471 | if entry.get("owned_root") and entry["owned_root"]: 472 | owned = "access_root" 473 | elif entry.get("owned_user") and entry["owned_user"]: 474 | owned = "access_user" 475 | else: 476 | owned = "access_none" 477 | writeup = to_emoji("has_writeup") if entry.get("writeups") and entry["writeups"].get("7h3rAm") else "" 478 | ttps = "\n".join([ 479 | ",".join([green(x) for x in entry["writeups"]["7h3rAm"]["ttps"]["enumerate"]]), 480 | ",".join([yellow(x) for x in entry["writeups"]["7h3rAm"]["ttps"]["exploit"]]), 481 | ",".join([red(x) for x in entry["writeups"]["7h3rAm"]["ttps"]["privesc"]]) 482 | ]).strip() if entry.get("writeups") and entry["writeups"].get("7h3rAm") else "" 483 | if showttps: 484 | rows.append("%s.___%s___%s___%s___%s___%s___%s___%s___%s___%s___%s" % ( 485 | idx+1, 486 | mid, 487 | name, 488 | entry["expires_at"], 489 | rating, 490 | to_emoji(difficulty), 491 | os, 492 | to_emoji(oscplike), 493 | to_emoji(owned), 494 | writeup, 495 | ttps, 496 | )) 497 | else: 498 | rows.append("%s.___%s___%s___%s___%s___%s___%s___%s___%s___%s" % ( 499 | idx+1, 500 | mid, 501 | name, 502 | entry["expires_at"], 503 | rating, 504 | to_emoji(difficulty), 505 | os, 506 | to_emoji(oscplike), 507 | to_emoji(owned), 508 | writeup, 509 | )) 510 | 511 | elif data[0].get("search_url"): 512 | header = ["#", "ID", "Name", "Follow", "Rating", "Difficulty", "OS", "OSCPlike", "Owned", "Writeup", "TTPs"] if showttps else ["#", "ID", "Name", "Follow", "Rating", "Difficulty", "OS", "OSCPlike", "Owned", "Writeup"] 513 | for idx, entry in enumerate(sorted(data, key=lambda k: k[sort_key].lower())): 514 | mid = "%s%s" % (blue("%s#" % (entry["verbose_id"].split("#")[0])), blue_bold("%s" % (entry["verbose_id"].split("#")[1]))) 515 | name = trim(entry["name"], maxq=30) 516 | match = trim(entry["search_text"].replace(" - ", " ").strip(), maxq=30) if entry.get("search_text") else "" 517 | if entry["search_url"].startswith("youtu.be/"): 518 | follow = "%s %s" % (red(""), blue(entry["search_url"])) 519 | else: 520 | follow = blue(entry["search_url"]) 521 | os = to_emoji(entry["os"]) 522 | difficulty = entry["difficulty"] if entry.get("difficulty") and entry["difficulty"] else "difficulty_unknown" 523 | rating = to_color_difficulty(sparkify(entry["difficulty_ratings"])) if entry.get("difficulty_ratings") else "" 524 | oscplike = "oscplike" if entry.get("oscplike") and entry["oscplike"] else "notoscplike" 525 | if entry.get("owned_root") and entry["owned_root"]: 526 | owned = "access_root" 527 | elif entry.get("owned_user") and entry["owned_user"]: 528 | owned = "access_user" 529 | else: 530 | owned = "access_none" 531 | writeup = to_emoji("has_writeup") if entry.get("writeups") and entry["writeups"].get("7h3rAm") else "" 532 | ttps = "\n".join([ 533 | ",".join([green(x) for x in entry["writeups"]["7h3rAm"]["ttps"]["enumerate"]]), 534 | ",".join([yellow(x) for x in entry["writeups"]["7h3rAm"]["ttps"]["exploit"]]), 535 | ",".join([red(x) for x in entry["writeups"]["7h3rAm"]["ttps"]["privesc"]]) 536 | ]).strip() if entry.get("writeups") and entry["writeups"].get("7h3rAm") else "" 537 | if showttps: 538 | rows.append("%s.___%s___%s___%s___%s___%s___%s___%s___%s___%s___%s" % ( 539 | idx+1, 540 | mid, 541 | name, 542 | follow, 543 | rating, 544 | to_emoji(difficulty), 545 | os, 546 | to_emoji(oscplike), 547 | to_emoji(owned), 548 | writeup, 549 | ttps, 550 | )) 551 | else: 552 | rows.append("%s.___%s___%s___%s___%s___%s___%s___%s___%s___%s" % ( 553 | idx+1, 554 | mid, 555 | name, 556 | follow, 557 | rating, 558 | to_emoji(difficulty), 559 | os, 560 | to_emoji(oscplike), 561 | to_emoji(owned), 562 | writeup, 563 | )) 564 | 565 | else: 566 | header = ["#", "ID", "Name", "Rating", "Difficulty", "OS", "OSCPlike", "Owned", "Writeup", "TTPs"] if showttps else ["#", "ID", "Name", "Rating", "Difficulty", "OS", "OSCPlike", "Owned", "Writeup"] 567 | for idx, entry in enumerate(sorted(data, key=lambda k: k[sort_key].lower())): 568 | mid = "%s%s" % (blue("%s#" % (entry["verbose_id"].split("#")[0])), blue_bold("%s" % (entry["verbose_id"].split("#")[1]))) 569 | name = trim(entry["name"], maxq=30) 570 | os = to_emoji(entry["os"]) 571 | difficulty = entry["difficulty"] if entry.get("difficulty") and entry["difficulty"] else "difficulty_unknown" 572 | rating = to_color_difficulty(sparkify(entry["difficulty_ratings"])) if entry.get("difficulty_ratings") else "" 573 | oscplike = "oscplike" if entry.get("oscplike") and entry["oscplike"] else "notoscplike" 574 | if entry.get("owned_root") and entry["owned_root"]: 575 | owned = "access_root" 576 | elif entry.get("owned_user") and entry["owned_user"]: 577 | owned = "access_user" 578 | else: 579 | owned = "access_none" 580 | writeup = to_emoji("has_writeup") if entry.get("writeups") and entry["writeups"].get("7h3rAm") else "" 581 | ttps = "\n".join([ 582 | ",".join([green(x) for x in entry["writeups"]["7h3rAm"]["ttps"]["enumerate"]]), 583 | ",".join([yellow(x) for x in entry["writeups"]["7h3rAm"]["ttps"]["exploit"]]), 584 | ",".join([red(x) for x in entry["writeups"]["7h3rAm"]["ttps"]["privesc"]]) 585 | ]).strip() if entry.get("writeups") and entry["writeups"].get("7h3rAm") else "" 586 | if showttps: 587 | rows.append("%s.___%s___%s___%s___%s___%s___%s___%s___%s___%s" % ( 588 | idx+1, 589 | mid, 590 | name, 591 | rating, 592 | to_emoji(difficulty), 593 | os, 594 | to_emoji(oscplike), 595 | to_emoji(owned), 596 | writeup, 597 | ttps, 598 | )) 599 | else: 600 | rows.append("%s.___%s___%s___%s___%s___%s___%s___%s___%s" % ( 601 | idx+1, 602 | mid, 603 | name, 604 | rating, 605 | to_emoji(difficulty), 606 | os, 607 | to_emoji(oscplike), 608 | to_emoji(owned), 609 | writeup, 610 | )) 611 | 612 | to_table(header=header, rows=rows, delim="___", aligndict=None, markdown=False, multiline=False) 613 | 614 | def to_xkcd(plotdict, filename, title, rotate=True, trimlength=20): 615 | datadict = {} 616 | for key in plotdict: 617 | datadict[key] = [[key], [plotdict[key]]] 618 | with plt.xkcd(): 619 | for idx, label in enumerate(datadict): 620 | plt.bar(datadict[label][0], datadict[label][1]) 621 | text = "%s... (%d)" % ("".join(datadict[label][0][0][:trimlength]), datadict[label][1][0]) if len(label) >= trimlength else "%s (%d)" % (datadict[label][0][0], datadict[label][1][0]) 622 | if rotate: 623 | angle = 90 624 | x, y = idx, 0.5 625 | else: 626 | angle = 0 627 | padding = (len(label)/2)/10 628 | x, y = idx-padding, datadict[label][1][0]-1 629 | plt.text(s=text, x=x, y=y, color="black", verticalalignment="center", horizontalalignment="left", size=15, rotation=angle, rotation_mode="anchor") 630 | plt.suptitle(title, fontsize=18, color="black") 631 | plt.gca().spines["left"].set_color("black") 632 | plt.gca().spines["bottom"].set_color("black") 633 | plt.gca().spines["left"].set_visible(False) 634 | plt.gca().spines["right"].set_visible(False) 635 | plt.gca().spines["top"].set_visible(False) 636 | plt.xticks([]); plt.yticks([]) 637 | plt.tight_layout() 638 | plt.savefig(filename, dpi=300) 639 | plt.close() 640 | 641 | def to_sparklines(items, filename, transparent=True): 642 | colormap = ["#9acc14", "#9acc14", "#9acc14", "#f7af3e", "#f7af3e", "#f7af3e", "#f7af3e", "#db524b", "#db524b", "#db524b"] 643 | barlist = plt.bar([str(x) for x in range(len(items))], items, width=0.95) 644 | for i in range(len(items)): 645 | barlist[i].set_color(colormap[i]) 646 | ax = plt.gca() 647 | ax.spines["bottom"].set_visible(False) 648 | ax.spines["left"].set_visible(False) 649 | ax.spines["right"].set_visible(False) 650 | ax.spines["top"].set_visible(False) 651 | plt.xticks([]); plt.yticks([]) 652 | plt.tight_layout() 653 | plt.savefig(filename, dpi=300, transparent=transparent) 654 | plt.close() 655 | -------------------------------------------------------------------------------- /svachal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import jq 4 | import os 5 | import sys 6 | import shutil 7 | import argparse 8 | import datetime 9 | import subprocess 10 | 11 | import yaml 12 | import jinja2 as jinja 13 | import markdown2 as markdown 14 | 15 | import utils 16 | import yml2dot 17 | 18 | 19 | class Svachal: 20 | def __init__(self, writeupdir, githubrepourl): 21 | self.config = {} 22 | 23 | self.config["writeupdir"] = writeupdir 24 | self.config["githubrepourl"] = githubrepourl 25 | # update the template.writeup.md and template.readme.md file with correct pdf metadata and github repo url 26 | 27 | self.config["basedir"] = os.path.dirname(os.path.realpath(__file__)) 28 | 29 | self.config["machinesjson"] = "%s/toolbox/bootstrap/machines.json" % (utils.expand_env(var="$HOME")) 30 | self.machinesstats = utils.load_json(self.config["machinesjson"]) 31 | 32 | self.config["metayml"] = "%s/meta.yml" % (self.config["writeupdir"]) 33 | self.config["summaryyml"] = "%s/summary.yml" % (self.config["writeupdir"]) 34 | 35 | self.config["templatedir"] = self.config["basedir"] 36 | self.config["templatefile"] = "template.writeup.md" 37 | self.config["templateyml"] = "%s/template.writeup.yml" % (self.config["basedir"]) 38 | 39 | self.config["topcount"] = 10 40 | self.config["ttpscsv"] = "%s/ttps.csv" % (self.config["writeupdir"]) 41 | 42 | self.infra = { 43 | "HTB": "HackTheBox", 44 | "THM": "TryHackMe", 45 | "VH": "VulnHub", 46 | } 47 | 48 | self.y2d = yml2dot.YML2DOT(fontsize="large", addrootnode=False, rankdirlr=False, randomnodecolor=False, savehtml=False) 49 | self.summary = None 50 | 51 | def _json_query(self, query): 52 | try: 53 | return jq.compile(query).input(self.machinesstats).all() 54 | except: 55 | return [] 56 | 57 | def md2pdf(self, destdir, mdname, pdfname): 58 | results = subprocess.run(['pandoc', '%s/%s' % (destdir, mdname), '-o', '%s/%s' % (destdir, pdfname), '--from', 'markdown+yaml_metadata_block+raw_html', '--highlight-style', 'tango', '--pdf-engine=xelatex'], cwd=destdir, stdout=subprocess.PIPE).stdout.decode('utf-8') 59 | 60 | def yml2md(self, ymlfile, templatefile, templatedir, destdir, destfile, ignoreprivate=False): 61 | dictyml = utils.load_yaml(ymlfile) 62 | 63 | if not ignoreprivate: 64 | if dictyml.get("writeup") and dictyml["writeup"]["metadata"]["status"].lower().strip() == "private": 65 | #utils.warn("writeup file '%s' is not marked for publishing (status == private)" % (ymlfile)) 66 | return 67 | 68 | env = jinja.Environment(loader=jinja.FileSystemLoader(templatedir), trim_blocks=True, lstrip_blocks=True) 69 | env.filters["datetimefilter"] = utils.datetimefilter 70 | env.filters["ghsearchlinks"] = utils.ghsearchlinks 71 | env.filters["anchorformat"] = utils.anchorformat 72 | env.filters["anchorformatttps"] = utils.anchorformatttps 73 | env.filters["mdurl"] = utils.mdurl 74 | env.filters["obfuscate"] = utils.obfuscate 75 | env.filters["monojoin"] = utils.monojoin 76 | env.filters["customsort"] = utils.customsort 77 | template = env.get_template(templatefile) 78 | rendermd = template.render(dictyml) 79 | destfilepath = "%s/%s" % (destdir, destfile) 80 | utils.file_save(destfilepath, rendermd) 81 | 82 | if "summary.yml" not in ymlfile: 83 | if dictyml["writeup"].get("overview") and dictyml["writeup"]["overview"]["killchain"]: 84 | try: 85 | parentdir = "/".join(ymlfile.split("/")[:-1]) 86 | dotfile = "%s/killchain.dot" % (parentdir) 87 | killchain = self.y2d.process(dictyml["writeup"]["overview"]["killchain"], dotfile) 88 | except Exception as ex: 89 | print("exception! failed to create overview killchain '%s'. please check below for more details:" % (destfile)) 90 | print(repr(ex)) 91 | 92 | try: 93 | self.md2pdf(destdir, destfile, "writeup.pdf") 94 | except Exception as ex: 95 | print("exception! md file '%s' could not be converted to pdf. please check below for more details:" % (destfile)) 96 | print(repr(ex)) 97 | 98 | return destfilepath 99 | 100 | def plot(self): 101 | utils.to_xkcd( 102 | plotdict=dict(sorted(sorted(self.summary["plot"]["ports"].items(), key=lambda x: x[1], reverse=True)[:self.config["topcount"]])), 103 | filename="%s/top_ports.png" % (self.config["writeupdir"]), 104 | title="Top Ports", 105 | rotate=True 106 | ) 107 | utils.to_xkcd( 108 | plotdict=dict(sorted(sorted(self.summary["plot"]["protocols"].items(), key=lambda x: x[1], reverse=True)[:self.config["topcount"]])), 109 | filename="%s/top_protocols.png" % (self.config["writeupdir"]), 110 | title="Top Protocols", 111 | rotate=True 112 | ) 113 | utils.to_xkcd( 114 | plotdict=dict(sorted(sorted(self.summary["plot"]["services"].items(), key=lambda x: x[1], reverse=True)[:self.config["topcount"]])), 115 | filename="%s/top_services.png" % (self.config["writeupdir"]), 116 | title="Top Services", 117 | rotate=True 118 | ) 119 | 120 | utils.to_xkcd( 121 | plotdict=dict(sorted(sorted(self.summary["plot"]["categories"].items(), key=lambda x: x[1], reverse=True)[:self.config["topcount"]])), 122 | filename="%s/top_categories.png" % (self.config["writeupdir"]), 123 | title="Top Categories", 124 | rotate=True 125 | ) 126 | 127 | plotdict = {k: v for k, v in self.summary["plot"]["ttps"].items() if k.startswith("enumerate_")} 128 | utils.to_xkcd( 129 | plotdict=dict(sorted(sorted(plotdict.items(), key=lambda x: x[1], reverse=True)[:self.config["topcount"]])), 130 | filename="%s/top_ttps_enumerate.png" % (self.config["writeupdir"]), 131 | title="Top TTPs - Phase #1 Enumeration", 132 | rotate=True 133 | ) 134 | plotdict = {k: v for k, v in self.summary["plot"]["ttps"].items() if k.startswith("exploit_")} 135 | utils.to_xkcd( 136 | plotdict=dict(sorted(sorted(plotdict.items(), key=lambda x: x[1], reverse=True)[:self.config["topcount"]])), 137 | filename="%s/top_ttps_exploit.png" % (self.config["writeupdir"]), 138 | title="Top TTPs - Phase #2 Exploitation", 139 | rotate=True 140 | ) 141 | plotdict = {k: v for k, v in self.summary["plot"]["ttps"].items() if k.startswith("privesc_")} 142 | utils.to_xkcd( 143 | plotdict=dict(sorted(sorted(plotdict.items(), key=lambda x: x[1], reverse=True)[:self.config["topcount"]])), 144 | filename="%s/top_ttps_privesc.png" % (self.config["writeupdir"]), 145 | title="Top TTPs - Phase #3 Privilege Escalation", 146 | rotate=True 147 | ) 148 | 149 | def url2metadata(self, url): 150 | def search_by_key(data, machines, key="url"): 151 | if machines and len(machines): 152 | for entry in machines: 153 | if key in ["name"]: 154 | if data in entry[key].lower().strip(): 155 | return entry 156 | if key in ["url"]: 157 | if data == entry[key].lower().strip(): 158 | return entry 159 | elif key in ["id"]: 160 | if data == entry[key]: 161 | return entry 162 | url = url.lower().strip() 163 | stats, infra = None, "" 164 | writeupyml = utils.load_yaml(self.config["templateyml"]) 165 | writeupyml["writeup"]["metadata"]["infocard"] = "./infocard.png" 166 | writeupyml["writeup"]["metadata"]["references"] = [""] 167 | writeupyml["writeup"]["metadata"]["status"] = "private" 168 | writeupyml["writeup"]["metadata"]["tags"] = ["enumerate_", "exploit_", "privesc_"] 169 | writeupyml["writeup"]["metadata"]["datetime"] = datetime.datetime.today().strftime("%Y%m%d") 170 | writeupyml["writeup"]["metadata"]["url"] = url 171 | writeupyml["writeup"]["metadata"]["name"] = "unknown" 172 | writeupyml["writeup"]["metadata"]["points"] = None 173 | writeupyml["writeup"]["metadata"]["categories"] = [] 174 | writeupyml["writeup"]["metadata"]["infra"] = "misc" 175 | writeupyml["writeup"]["metadata"]["path"] = "misc.unknown" 176 | stats = search_by_key(url, self.machinesstats["machines"], key="url") 177 | if stats: 178 | writeupyml["writeup"]["metadata"]["name"] = "%s" % (stats["name"]) 179 | writeupyml["writeup"]["metadata"]["points"] = stats["points"] if stats.get("points") and stats["points"] else None 180 | writeupyml["writeup"]["metadata"]["matrix"] = stats["matrix"] if stats.get("matrix") and stats["matrix"] else None 181 | if stats["os"]: 182 | writeupyml["writeup"]["metadata"]["categories"].append(stats["os"].lower()) 183 | if stats["oscplike"]: 184 | writeupyml["writeup"]["metadata"]["categories"].append("oscp") 185 | if stats["infrastructure"] in ["htb", "hackthebox"]: 186 | writeupyml["writeup"]["metadata"]["infra"] = "HackTheBox" 187 | writeupyml["writeup"]["metadata"]["categories"].append("hackthebox") 188 | writeupyml["writeup"]["metadata"]["path"] = "htb.%s" % (utils.cleanup_name(stats["shortname"])) 189 | elif stats["infrastructure"] in ["vh", "vulnhub"]: 190 | writeupyml["writeup"]["metadata"]["infra"] = "VulnHub" 191 | writeupyml["writeup"]["metadata"]["categories"].append("vulnhub") 192 | writeupyml["writeup"]["metadata"]["path"] = "vulnhub.%s" % (utils.cleanup_name(stats["shortname"])) 193 | elif stats["infrastructure"] in ["thm", "tryhackme"]: 194 | writeupyml["writeup"]["metadata"]["infra"] = "TryHackMe" 195 | writeupyml["writeup"]["metadata"]["categories"].append("tryhackme") 196 | writeupyml["writeup"]["metadata"]["path"] = "thm.%s" % (utils.cleanup_name(stats["shortname"])) 197 | return writeupyml["writeup"]["metadata"] 198 | 199 | def metadata2yml(self, metadata): 200 | infra = "" 201 | if metadata["infra"]: 202 | try: 203 | infra = self.infra[metadata["infra"]] 204 | except: 205 | infra = metadata["infra"] 206 | 207 | formattedyml = """writeup: 208 | metadata: 209 | status: %s 210 | datetime: %s 211 | infra: %s 212 | name: %s 213 | points: %s 214 | path: %s 215 | url: %s 216 | infocard: %s 217 | references: 218 | %s 219 | categories: 220 | %s 221 | tags: 222 | %s 223 | overview: 224 | description: | 225 | This is a writeup for %s VM [`%s`](%s). Here's an overview of the `enumeration` → `exploitation` → `privilege escalation` process:""" % ( 226 | metadata["status"] if metadata["status"] else "", 227 | metadata["datetime"] if metadata["datetime"] else "", 228 | metadata["infra"] if metadata["infra"] else "", 229 | metadata["name"] if metadata["name"] else "", 230 | metadata["points"] if metadata["points"] else "", 231 | metadata["path"] if metadata["path"] else "", 232 | metadata["url"] if metadata["url"] else "", 233 | metadata["infocard"] if metadata["infocard"] else "", 234 | "\n".join([" - %s" % (x) for x in metadata["references"]]) if metadata["references"] else " - ", 235 | "\n".join([" - %s" % (x) for x in metadata["categories"]]) if metadata["categories"] else " - ", 236 | "\n".join([" - %s" % (x) for x in metadata["tags"]]) if metadata["tags"] else " - ", 237 | infra, 238 | metadata["name"] if metadata["name"] else "", 239 | metadata["url"] if metadata["url"] else "", 240 | ) 241 | return formattedyml 242 | 243 | def opcode_start(self, args, manual=False): 244 | if manual: 245 | if len(args.split(".", 1)) != 2: 246 | return 247 | metadata = {} 248 | metadata["status"] = "private" 249 | metadata["tags"] = ["enumerate_", "exploit_", "privesc_"] 250 | metadata["datetime"] = datetime.datetime.today().strftime("%Y%m%d") 251 | metadata["infra"] = args.split(".", 1)[0].upper() 252 | metadata["name"] = args.split(".", 1)[1].title() 253 | metadata["points"] = None 254 | metadata["path"] = args.lower() 255 | metadata["url"] = None 256 | metadata["infocard"] = None 257 | metadata["references"] = None 258 | metadata["categories"] = ["oscp", "vulnhub/htb/thm", "linux/windows/bsd"] 259 | self.config["destdirname"] = args 260 | else: 261 | url = args 262 | metadata = self.url2metadata(url) 263 | print(self.metadata2yml(metadata)) 264 | print() 265 | self.config["destdirname"] = metadata["path"] 266 | 267 | self.config["destdirpath"] = "%s/%s" % (self.config["writeupdir"], self.config["destdirname"]) 268 | self.config["writeupyml"] = "%s/writeup.yml" % (self.config["destdirpath"]) 269 | if not os.path.isfile(self.config["writeupyml"]): 270 | utils.mkdirp(self.config["destdirpath"]) 271 | writeupyml = utils.file_open(self.config["templateyml"]) 272 | updatedwriteupyml = "%s\n%s" % (self.metadata2yml(metadata), "\n".join(writeupyml.split("\n")[23:])) 273 | utils.file_save(self.config["writeupyml"], updatedwriteupyml) 274 | utils.info("writeup file '%s' created for target '%s'" % (self.config["writeupyml"], self.config["destdirname"])) 275 | for machine in self.machinesstats["machines"]: 276 | if machine["url"] == metadata["url"]: 277 | 278 | if machine.get("difficulty_ratings") and machine["difficulty_ratings"]: 279 | utils.to_sparklines(machine["difficulty_ratings"] if machine["difficulty_ratings"] else [], filename="%s/ratings.png" % (self.config["destdirpath"])) 280 | utils.info("created '%s/ratings.png' file for target '%s'" % (self.config["destdirpath"], self.config["destdirname"])) 281 | 282 | if machine.get("matrix") and machine["matrix"]: 283 | url = "https://quickchart.io/chart?bkg=rgba(255,255,255,0.2)&width=270&height=200&c={ type: 'radar', data: {fill: 'False', labels: ['Enumeration', 'Real-Life', 'CVE', ['Custom', 'Exploitation'], 'CTF-Like'], datasets: [{ label: 'User rated', data: %s, backgroundColor:'rgba(154,204,20,0.2)', borderColor:'rgb(154,204,20)', pointBackgroundColor:'rgb(154,204,20)' }, { label: 'Maker rated', data: %s, backgroundColor:'rgba(86,192,224,0.2)', borderColor:'rgb(86,192,224)', pointBackgroundColor:'rgb(86,192,224)' }] }, options: { layout:{ padding:25}, plugins: { legend: false,}, scale: { angleLines: { display: true, color: 'rgb(21,23,25)' }, ticks: { callback: function() {return ''}, backdropColor: 'rgba(0, 0, 0, 0)' }, gridLines: { color: 'rgb(51,54,60)', } }, } }" % (machine["matrix"]["aggregate"], machine["matrix"]["maker"]) 284 | res = utils.get_http_res(url, requoteuri=True) 285 | filename = "%s/matrix.png" % (self.config["destdirpath"]) 286 | with open(filename, "wb") as fp: 287 | fp.write(res.content) 288 | utils.info("created '%s/matrix.png' file for target '%s'" % (self.config["destdirpath"], self.config["destdirname"])) 289 | 290 | else: 291 | utils.warn("writeup file '%s' already exists for target '%s'" % (self.config["writeupyml"], self.config["destdirname"])) 292 | 293 | def opcode_finish(self): 294 | self.config["destdirpath"] = "." 295 | self.config["writeupyml"] = "%s/writeup.yml" % (self.config["destdirpath"]) 296 | if not os.path.isfile(self.config["writeupyml"]): 297 | utils.error("could not find writeup file '%s'" % (self.config["writeupyml"])) 298 | else: 299 | self.yml2md(self.config["writeupyml"], self.config["templatefile"], self.config["templatedir"], self.config["destdirpath"], "writeup.md", ignoreprivate=True) 300 | 301 | def opcode_rebuildall(self): 302 | writeupdirs = [x.replace("/writeup.yml", "").split("/")[-1] for x in utils.search_files_yml(self.config["writeupdir"])] 303 | total = len(writeupdirs) 304 | privatewriteups = [] 305 | for idx, wd in enumerate(sorted(writeupdirs, key=str.casefold)): 306 | destdirpath = "%s/%s" % (self.config["writeupdir"], wd) 307 | writeupyml = "%s/writeup.yml" % (destdirpath) 308 | destfilepath = self.yml2md(writeupyml, self.config["templatefile"], self.config["templatedir"], destdirpath, "writeup.md") 309 | print("(%03d/%03d) '%s' → '%s'" % (idx+1, total, writeupyml, destfilepath)) 310 | if not destfilepath: 311 | privatewriteups.append(writeupyml) 312 | utils.info("rebuilt %d writeups @ %s (private: %d)" % (total-len(privatewriteups), self.config["writeupdir"], len(privatewriteups))) 313 | 314 | def opcode_summarize(self): 315 | metadict = utils.load_yaml(self.config["metayml"]) 316 | self.summary = { 317 | "stats": { 318 | "counts": "", 319 | "owned": "", 320 | }, 321 | "counts": { 322 | "totalvh": 0, 323 | "totalhtb": 0, 324 | "totalthm": 0, 325 | "totalnix": 0, 326 | "totalwindows": 0, 327 | "total": 0, 328 | "writeups": 0, 329 | "percent": 0.0, 330 | "writeupsnix": 0, 331 | "writeupswindows": 0, 332 | "percentnix": 0.0, 333 | "percentwindows": 0.0, 334 | "writeupsvh": 0, 335 | "writeupshtb": 0, 336 | "writeupsthm": 0, 337 | "percentvh": 0.0, 338 | "percenthtb": 0.0, 339 | "percentthm": 0.0, 340 | }, 341 | "loot": { 342 | "hashes": [], 343 | "credentials": [], 344 | }, 345 | "plot": { 346 | "ports": {}, 347 | "protocols": {}, 348 | "services": {}, 349 | "ttps": {}, 350 | "categories": {}, 351 | }, 352 | "readme": [], 353 | "ttpsitw": {}, 354 | "techniques": { 355 | "enumerate": metadict["meta"]["ttps"]["enumerate"], 356 | "exploit": metadict["meta"]["ttps"]["exploit"], 357 | "privesc": metadict["meta"]["ttps"]["privesc"], 358 | }, 359 | "tips": metadict["meta"]["tips"], 360 | "tools": metadict["meta"]["tools"], 361 | "methodology": metadict["meta"]["methodology"], 362 | } 363 | 364 | for key in self.machinesstats["counts"]: 365 | self.summary["counts"][key] = self.machinesstats["counts"][key] 366 | 367 | writeupdirs = [x.replace("/writeup.yml", "").split("/")[-1] for x in utils.search_files_yml(self.config["writeupdir"])] 368 | total, private, machines = len(writeupdirs), [], [] 369 | for idx, wd in enumerate(sorted(writeupdirs, key=str.casefold)): 370 | destdirpath = "%s/%s" % (self.config["writeupdir"], wd) 371 | writeupyml = "%s/writeup.yml" % (destdirpath) 372 | print("%s summarizing '%s'" % (utils.blue_bold("(%03d/%03d)" % (idx+1, total)), writeupyml)) 373 | dictyml = utils.load_yaml(writeupyml) 374 | 375 | writeupmdurl = "%s/blob/master/%s/writeup.md" % (self.config["githubrepourl"], dictyml["writeup"]["metadata"]["path"]) 376 | writeuppdfurl = "%s/blob/master/%s/writeup.pdf" % (self.config["githubrepourl"], dictyml["writeup"]["metadata"]["path"]) 377 | killchainurl = "%s/blob/master/%s/killchain.png" % (self.config["githubrepourl"], dictyml["writeup"]["metadata"]["path"]) 378 | 379 | dictyml["writeup"]["machine"] = {} 380 | query = '.machines[] | select(.url == "%s")' % (dictyml["writeup"]["metadata"]["url"]) 381 | result = self._json_query(query) 382 | if result and len(result) == 1: 383 | machinestats = result[0] 384 | else: 385 | machinestats = { 386 | "name": dictyml["writeup"]["metadata"]["name"], 387 | "url": dictyml["writeup"]["metadata"]["url"], 388 | } 389 | dictyml["writeup"]["machine"] = machinestats 390 | dictyml["writeup"]["machine"]["writeupmdurl"] = writeupmdurl 391 | dictyml["writeup"]["machine"]["writeuppdfurl"] = writeuppdfurl 392 | dictyml["writeup"]["machine"]["killchainurl"] = killchainurl 393 | dictyml["writeup"]["machine"]["verbose_id"] = machinestats["verbose_id"] if "verbose_id" in machinestats else "%s#%s" % (dictyml["writeup"]["metadata"]["infra"].lower().strip(), dictyml["writeup"]["metadata"]["name"].replace(" ", "").lower().strip()) 394 | if "oscplike" in machinestats: 395 | dictyml["writeup"]["machine"]["oscplike"] = machinestats["oscplike"] 396 | elif "oscp" in dictyml["writeup"]["metadata"]["categories"]: 397 | dictyml["writeup"]["machine"]["oscplike"] = True 398 | else: 399 | dictyml["writeup"]["machine"]["oscplike"] = False 400 | dictyml["writeup"]["machine"]["os"] = machinestats["os"] if "os" in machinestats else None 401 | dictyml["writeup"]["machine"]["private"] = True if dictyml["writeup"]["metadata"]["status"].lower().strip() == "private" else False 402 | dictyml["writeup"]["machine"]["owned_user"] = True if dictyml["writeup"]["metadata"]["status"].lower().strip() == "public" else False 403 | dictyml["writeup"]["machine"]["owned_root"] = True if dictyml["writeup"]["metadata"]["status"].lower().strip() == "public" else False 404 | if dictyml["writeup"]["machine"].get("difficulty_ratings") and dictyml["writeup"]["machine"]["difficulty_ratings"]: 405 | dictyml["writeup"]["machine"]["ratingsurl"] = "%s/blob/master/%s/ratings.png" % (self.config["githubrepourl"], dictyml["writeup"]["metadata"]["path"]) 406 | dictyml["writeup"]["machine"]["matrixurl"] = "%s/blob/master/%s/matrix.png" % (self.config["githubrepourl"], dictyml["writeup"]["metadata"]["path"]) 407 | else: 408 | dictyml["writeup"]["machine"]["ratingsurl"] = None 409 | dictyml["writeup"]["machine"]["matrixurl"] = None 410 | 411 | machines.append(dictyml["writeup"]["machine"]) 412 | 413 | if dictyml["writeup"]["metadata"]["status"].lower().strip() == "private": 414 | private.append(dictyml) 415 | continue 416 | 417 | if ("vulnhub" in dictyml["writeup"]["metadata"]["categories"] or "vh" in dictyml["writeup"]["metadata"]["categories"]) and "linux" in dictyml["writeup"]["metadata"]["categories"]: 418 | self.summary["counts"]["writeupsvh"] += 1 419 | self.summary["counts"]["writeupsnix"] += 1 420 | if ("vulnhub" in dictyml["writeup"]["metadata"]["categories"] or "vh" in dictyml["writeup"]["metadata"]["categories"]) and "windows" in dictyml["writeup"]["metadata"]["categories"]: 421 | self.summary["counts"]["writeupsvh"] += 1 422 | self.summary["counts"]["writeupswindows"] += 1 423 | if ("hackthebox" in dictyml["writeup"]["metadata"]["categories"] or "htb" in dictyml["writeup"]["metadata"]["categories"]) and "linux" in dictyml["writeup"]["metadata"]["categories"]: 424 | self.summary["counts"]["writeupshtb"] += 1 425 | self.summary["counts"]["writeupsnix"] += 1 426 | if ("hackthebox" in dictyml["writeup"]["metadata"]["categories"] or "htb" in dictyml["writeup"]["metadata"]["categories"]) and "windows" in dictyml["writeup"]["metadata"]["categories"]: 427 | self.summary["counts"]["writeupshtb"] += 1 428 | self.summary["counts"]["writeupswindows"] += 1 429 | if ("tryhackme" in dictyml["writeup"]["metadata"]["categories"] or "thm" in dictyml["writeup"]["metadata"]["categories"]) and "linux" in dictyml["writeup"]["metadata"]["categories"]: 430 | self.summary["counts"]["writeupsthm"] += 1 431 | self.summary["counts"]["writeupsnix"] += 1 432 | if ("tryhackme" in dictyml["writeup"]["metadata"]["categories"] or "thm" in dictyml["writeup"]["metadata"]["categories"]) and "windows" in dictyml["writeup"]["metadata"]["categories"]: 433 | self.summary["counts"]["writeupsthm"] += 1 434 | self.summary["counts"]["writeupswindows"] += 1 435 | self.summary["counts"]["writeups"] = self.summary["counts"]["writeupsvh"] + self.summary["counts"]["writeupshtb"] 436 | self.summary["counts"]["percent"] = "%.2f" % ((self.summary["counts"]["writeups"] / self.summary["counts"]["totaloscplike"]) * 100) 437 | self.summary["counts"]["percentvh"] = "%.2f" % ((self.summary["counts"]["writeupsvh"] / self.summary["counts"]["vhoscplike"]) * 100) 438 | self.summary["counts"]["percenthtb"] = "%.2f" % ((self.summary["counts"]["writeupshtb"] / self.summary["counts"]["htboscplike"]) * 100) 439 | self.summary["counts"]["percentthm"] = "%.2f" % ((self.summary["counts"]["writeupsthm"] / self.summary["counts"]["thmoscplike"]) * 100) 440 | self.summary["counts"]["percentnix"] = "%.2f" % ((self.summary["counts"]["writeupsnix"] / self.summary["counts"]["totalnix"]) * 100) 441 | self.summary["counts"]["percentwindows"] = "%.2f" % ((self.summary["counts"]["writeupswindows"] / self.summary["counts"]["totalwindows"]) * 100) 442 | 443 | # uncomment lines below if matrix.png has to be updated 444 | #if machine.get("matrix"): 445 | # utils.to_sparklines(machine["difficulty_ratings"] if machine["difficulty_ratings"] else [], filename="%s/ratings.png" % (destdirpath)) 446 | # url = "https://quickchart.io/chart?bkg=rgba(255,255,255,0.2)&width=270&height=200&c={ type: 'radar', data: {fill: 'False', labels: ['Enumeration', 'Real-Life', 'CVE', ['Custom', 'Exploitation'], 'CTF-Like'], datasets: [{ label: 'User rated', data: %s, backgroundColor:'rgba(154,204,20,0.2)', borderColor:'rgb(154,204,20)', pointBackgroundColor:'rgb(154,204,20)' }, { label: 'Maker rated', data: %s, backgroundColor:'rgba(86,192,224,0.2)', borderColor:'rgb(86,192,224)', pointBackgroundColor:'rgb(86,192,224)' }] }, options: { layout:{ padding:25}, plugins: { legend: false,}, scale: { angleLines: { display: true, color: 'rgb(21,23,25)' }, ticks: { callback: function() {return ''}, backdropColor: 'rgba(0, 0, 0, 0)' }, gridLines: { color: 'rgb(51,54,60)', } }, } }" % (machine["matrix"]["aggregate"], machine["matrix"]["maker"]) 447 | # res = utils.get_http_res(url, requoteuri=True) 448 | # filename = "%s/matrix.png" % (destdirpath) 449 | # with open(filename, "wb") as fp: 450 | # fp.write(res.content) 451 | 452 | for tag in dictyml["writeup"]["metadata"]["tags"]: 453 | if tag.startswith("enumerate_"): 454 | if None in self.summary["techniques"]["enumerate"][tag]["references"]: 455 | self.summary["techniques"]["enumerate"][tag]["references"] = [] 456 | if "writeups" not in self.summary["techniques"]["enumerate"][tag]: 457 | self.summary["techniques"]["enumerate"][tag]["writeups"] = [] 458 | self.summary["techniques"]["enumerate"][tag]["writeups"].append({ 459 | "status": dictyml["writeup"]["metadata"]["status"], 460 | "datetime": dictyml["writeup"]["metadata"]["datetime"], 461 | "name": dictyml["writeup"]["metadata"]["name"], 462 | "url": dictyml["writeup"]["metadata"]["url"], 463 | "infra": dictyml["writeup"]["metadata"]["infra"], 464 | "points": dictyml["writeup"]["metadata"]["points"] if dictyml["writeup"]["metadata"].get("points") else None, 465 | "tags": dictyml["writeup"]["metadata"]["tags"], 466 | "writeup": writeuppdfurl, 467 | "verbose_id": dictyml["writeup"]["machine"]["verbose_id"].replace("hackthebox", "htb").replace("vulnhub", "vh"), 468 | "overview": '' % (killchainurl), 469 | }) 470 | if tag.startswith("exploit_"): 471 | if None in self.summary["techniques"]["exploit"][tag]["references"]: 472 | self.summary["techniques"]["exploit"][tag]["references"] = [] 473 | if "writeups" not in self.summary["techniques"]["exploit"][tag]: 474 | self.summary["techniques"]["exploit"][tag]["writeups"] = [] 475 | self.summary["techniques"]["exploit"][tag]["writeups"].append({ 476 | "status": dictyml["writeup"]["metadata"]["status"], 477 | "datetime": dictyml["writeup"]["metadata"]["datetime"], 478 | "name": dictyml["writeup"]["metadata"]["name"], 479 | "url": dictyml["writeup"]["metadata"]["url"], 480 | "infra": dictyml["writeup"]["metadata"]["infra"], 481 | "points": dictyml["writeup"]["metadata"]["points"] if dictyml["writeup"]["metadata"].get("points") else None, 482 | "tags": dictyml["writeup"]["metadata"]["tags"], 483 | "verbose_id": dictyml["writeup"]["machine"]["verbose_id"].replace("hackthebox", "htb").replace("vulnhub", "vh"), 484 | "writeup": writeuppdfurl, 485 | "overview": '' % (killchainurl if dictyml["writeup"].get("overview") else killchainurl), 486 | }) 487 | if tag.startswith("privesc_"): 488 | if None in self.summary["techniques"]["privesc"][tag]["references"]: 489 | self.summary["techniques"]["privesc"][tag]["references"] = [] 490 | if "writeups" not in self.summary["techniques"]["privesc"][tag]: 491 | self.summary["techniques"]["privesc"][tag]["writeups"] = [] 492 | self.summary["techniques"]["privesc"][tag]["writeups"].append({ 493 | "status": dictyml["writeup"]["metadata"]["status"], 494 | "datetime": dictyml["writeup"]["metadata"]["datetime"], 495 | "name": dictyml["writeup"]["metadata"]["name"], 496 | "url": dictyml["writeup"]["metadata"]["url"], 497 | "infra": dictyml["writeup"]["metadata"]["infra"], 498 | "points": dictyml["writeup"]["metadata"]["points"] if dictyml["writeup"]["metadata"].get("points") else None, 499 | "tags": dictyml["writeup"]["metadata"]["tags"], 500 | "verbose_id": dictyml["writeup"]["machine"]["verbose_id"].replace("hackthebox", "htb").replace("vulnhub", "vh"), 501 | "writeup": writeuppdfurl, 502 | "overview": '' % (killchainurl if dictyml["writeup"].get("overview") else killchainurl), 503 | }) 504 | 505 | if dictyml["writeup"]["overview"].get("ttps"): 506 | for protokey in dictyml["writeup"]["overview"]["ttps"]: 507 | port, l4, proto, service = None, None, None, None 508 | try: 509 | port, l4, proto, service = protokey.split("/", 3) 510 | except: 511 | try: 512 | port, l4, proto = protokey.split("/", 2) 513 | except: 514 | port, l4 = protokey.split("/", 1) 515 | 516 | pl4 = "%s/%s" % (port, l4) 517 | if pl4 in self.summary["plot"]["ports"]: 518 | self.summary["plot"]["ports"][pl4] += 1 519 | else: 520 | self.summary["plot"]["ports"][pl4] = 1 521 | 522 | if proto: 523 | if proto in self.summary["plot"]["protocols"]: 524 | self.summary["plot"]["protocols"][proto] += 1 525 | else: 526 | self.summary["plot"]["protocols"][proto] = 1 527 | 528 | if service: 529 | if service in self.summary["plot"]["services"]: 530 | self.summary["plot"]["services"][service] += 1 531 | else: 532 | self.summary["plot"]["services"][service] = 1 533 | 534 | for cat in dictyml["writeup"]["metadata"]["categories"]: 535 | if cat in self.summary["plot"]["categories"]: 536 | self.summary["plot"]["categories"][cat] += 1 537 | else: 538 | self.summary["plot"]["categories"][cat] = 1 539 | 540 | for tag in dictyml["writeup"]["metadata"]["tags"]: 541 | if tag in self.summary["plot"]["ttps"]: 542 | self.summary["plot"]["ttps"][tag] += 1 543 | else: 544 | self.summary["plot"]["ttps"][tag] = 1 545 | 546 | self.summary["readme"].append({ 547 | "status": dictyml["writeup"]["metadata"]["status"], 548 | "datetime": dictyml["writeup"]["metadata"]["datetime"], 549 | "name": dictyml["writeup"]["metadata"]["name"], 550 | "url": dictyml["writeup"]["metadata"]["url"], 551 | "infra": dictyml["writeup"]["metadata"]["infra"], 552 | "points": dictyml["writeup"]["metadata"]["points"] if dictyml["writeup"]["metadata"].get("points") else None, 553 | "tags": dictyml["writeup"]["metadata"]["tags"], 554 | "writeup": writeuppdfurl, 555 | "overview": '' % (killchainurl if dictyml["writeup"].get("overview") else killchainurl), 556 | "machine": dictyml["writeup"]["machine"], 557 | }) 558 | 559 | if dictyml["writeup"]["overview"].get("ttps"): 560 | for protokey in dictyml["writeup"]["overview"]["ttps"]: 561 | port, l4, proto, service = None, None, None, None 562 | try: 563 | port, l4, proto, service = protokey.split("/", 3) 564 | except: 565 | try: 566 | port, l4, proto = protokey.split("/", 2) 567 | except: 568 | port, l4 = protokey.split("/", 1) 569 | ttpsitw = dictyml["writeup"]["overview"]["ttps"][protokey].split(" ") 570 | 571 | protokey = "/".join(protokey.split("/")[2:]) 572 | if protokey == "": 573 | protokey = None 574 | 575 | if port not in self.summary["ttpsitw"]: 576 | self.summary["ttpsitw"][port] = { 577 | "port": port, 578 | "l4": l4, 579 | "ttps": [], 580 | "ttpsitw": ttpsitw, 581 | "protokeys": [protokey] if protokey else [], 582 | "writeups": [{"name": dictyml["writeup"]["metadata"]["name"], "verbose_id": dictyml["writeup"]["machine"]["verbose_id"], "url": writeuppdfurl}] if dictyml["writeup"]["machine"] else [], 583 | } 584 | else: 585 | self.summary["ttpsitw"][port]["ttpsitw"].extend(ttpsitw) 586 | if protokey: 587 | self.summary["ttpsitw"][port]["protokeys"].append(protokey) 588 | self.summary["ttpsitw"][port]["writeups"].append({"name": dictyml["writeup"]["metadata"]["name"], "verbose_id": dictyml["writeup"]["machine"]["verbose_id"], "url": writeuppdfurl}) 589 | 590 | self.summary["ttpsitw"][port]["ttpsitw"] = sorted(list(set(self.summary["ttpsitw"][port]["ttpsitw"])), key=str.casefold) 591 | self.summary["ttpsitw"][port]["protokeys"] = sorted(list(set(self.summary["ttpsitw"][port]["protokeys"])), key=str.casefold) 592 | 593 | loot = [] 594 | if dictyml["writeup"].get("loot") and dictyml["writeup"]["loot"].get("credentials"): 595 | for credtype in dictyml["writeup"]["loot"]["credentials"]: 596 | for entry in dictyml["writeup"]["loot"]["credentials"][credtype]: 597 | try: 598 | username, password = entry.split("/", 1) 599 | except: 600 | username, password = None, entry 601 | uniqkey = "%s,%s,%s" % (username, password, credtype) 602 | if uniqkey not in loot: 603 | self.summary["loot"]["credentials"].append({ 604 | "username": username, 605 | "password": password, 606 | "credtype": credtype, 607 | }) 608 | loot.append(uniqkey) 609 | 610 | if dictyml["writeup"].get("loot") and dictyml["writeup"]["loot"].get("hashes"): 611 | self.summary["loot"]["hashes"].extend([x for x in dictyml["writeup"]["loot"]["hashes"]]) 612 | self.summary["loot"]["hashes"] = sorted(list(set(self.summary["loot"]["hashes"])), key=str.casefold) 613 | 614 | # add writeup tags/ttps to machine entries in machines.json 615 | ttpmachines = [] 616 | for machine in self.machinesstats["machines"]: 617 | entry = machine 618 | if entry["url"] == dictyml["writeup"]["metadata"]["url"]: 619 | if not entry.get("writeups"): 620 | entry["writeups"] = {} 621 | entry["writeups"]["7h3rAm"] = { 622 | "ttps": { 623 | "enumerate": [], 624 | "exploit": [], 625 | "privesc": [], 626 | } 627 | } 628 | for tag in dictyml["writeup"]["metadata"]["tags"]: 629 | if tag.startswith("enumerate_"): entry["writeups"]["7h3rAm"]["ttps"]["enumerate"].append(tag) 630 | if tag.startswith("exploit_"): entry["writeups"]["7h3rAm"]["ttps"]["exploit"].append(tag) 631 | if tag.startswith("privesc_"): entry["writeups"]["7h3rAm"]["ttps"]["privesc"].append(tag) 632 | ttpmachines.append(entry) 633 | self.machinesstats["machines"] = ttpmachines 634 | utils.save_json(self.machinesstats, self.config["machinesjson"]) 635 | 636 | for ttp in self.summary["techniques"]["enumerate"]: 637 | if self.summary["techniques"]["enumerate"][ttp].get("ports"): 638 | for port in self.summary["techniques"]["enumerate"][ttp]["ports"]: 639 | if not port: 640 | continue 641 | port, l4 = port.split("/") 642 | if port not in self.summary["ttpsitw"]: 643 | self.summary["ttpsitw"][port] = { 644 | "port": port, 645 | "l4": l4, 646 | "ttps": [ttp], 647 | "ttpsitw": [], 648 | "protokeys": [], 649 | "writeups": [], 650 | } 651 | elif ttp not in self.summary["ttpsitw"][port]["ttps"]: 652 | self.summary["ttpsitw"][port]["ttps"].append(ttp) 653 | for ttp in self.summary["techniques"]["exploit"]: 654 | if self.summary["techniques"]["exploit"][ttp].get("ports"): 655 | for port in self.summary["techniques"]["exploit"][ttp]["ports"]: 656 | if not port: 657 | continue 658 | port, l4 = port.split("/") 659 | if port not in self.summary["ttpsitw"]: 660 | self.summary["ttpsitw"][port] = { 661 | "port": port, 662 | "l4": l4, 663 | "ttps": [ttp], 664 | "ttpsitw": [], 665 | "protokeys": [], 666 | "writeups": [], 667 | } 668 | elif ttp not in self.summary["ttpsitw"][port]["ttps"]: 669 | self.summary["ttpsitw"][port]["ttps"].append(ttp) 670 | for ttp in self.summary["techniques"]["privesc"]: 671 | if self.summary["techniques"]["privesc"][ttp].get("ports"): 672 | for port in self.summary["techniques"]["privesc"][ttp]["ports"]: 673 | if not port: 674 | continue 675 | port, l4 = port.split("/") 676 | if port not in self.summary["ttpsitw"]: 677 | self.summary["ttpsitw"][port] = { 678 | "port": port, 679 | "l4": l4, 680 | "ttps": [ttp], 681 | "ttpsitw": [], 682 | "protokeys": [], 683 | "writeups": [], 684 | } 685 | elif ttp not in self.summary["ttpsitw"][port]["ttps"]: 686 | self.summary["ttpsitw"][port]["ttps"].append(ttp) 687 | 688 | utils.show_machines(machines) 689 | 690 | self.plot() 691 | self.summary["stats"]["counts"] = self.stats_counts() 692 | self.summary["stats"]["owned"] = self.stats_owned(machines) 693 | 694 | ttpitwcsv = ["%s/%s,%s,%s,%s,%s" % ( 695 | port, 696 | self.summary["ttpsitw"][port]["l4"], 697 | ";".join([x.replace(";", "") for x in self.summary["ttpsitw"][port]["protokeys"]]), 698 | ";".join(self.summary["ttpsitw"][port]["ttps"]), 699 | ";".join(self.summary["ttpsitw"][port]["ttpsitw"]), 700 | ";".join(["%s (%s)" % (x["name"], x["verbose_id"]) for x in self.summary["ttpsitw"][port]["writeups"]])) for port in self.summary["ttpsitw"]] 701 | 702 | with open(self.config["ttpscsv"], "w") as fp: 703 | fp.write("\n".join(ttpitwcsv)) 704 | fp.write("\n") 705 | utils.info("updated %s with %d port-ttps mappings" % (self.config["ttpscsv"], len(ttpitwcsv))) 706 | 707 | utils.file_save(self.config["summaryyml"], utils.dict2yaml({"summary": self.summary})) 708 | utils.info("updated %s for %d writeups (private: %d)" % (self.config["summaryyml"], total-len(private), len(private))) 709 | 710 | self.yml2md(ymlfile=self.config["summaryyml"], templatefile="template.readme.md", templatedir=self.config["templatedir"], destdir=self.config["writeupdir"], destfile="readme.md") 711 | utils.info("updated %s/readme.md with new stats and metadata" % (self.config["writeupdir"])) 712 | 713 | def stats_counts(self): 714 | header, rows = ["#", "TryHackMe", "HackTheBox", "VulnHub", "OSCPlike", "Owned"], [] 715 | rows.append("___".join([x for x in [ 716 | "%s" % ("Total"), 717 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedthm"], self.summary["counts"]["totalthm"], "%.2f%%" % (self.summary["counts"]["perthm"])), 718 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedhtb"], self.summary["counts"]["totalhtb"], "%.2f%%" % (self.summary["counts"]["perhtb"])), 719 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedvh"], self.summary["counts"]["totalvh"], "%.2f%%" % (self.summary["counts"]["pervh"])), 720 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedoscplike"], self.summary["counts"]["totaloscplike"], "%.2f%%" % (self.summary["counts"]["peroscplike"])), 721 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedtotal"], self.summary["counts"]["totaltotal"], "%.2f%%" % (self.summary["counts"]["pertotal"])), 722 | ]])) 723 | rows.append("___".join([str(x) for x in [ 724 | "Windows", 725 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedthmwindows"], self.summary["counts"]["thmwindows"], "%.2f%%" % (self.summary["counts"]["perthmwindows"])), 726 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedhtbwindows"], self.summary["counts"]["htbwindows"], "%.2f%%" % (self.summary["counts"]["perhtbwindows"])), 727 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedvhwindows"], self.summary["counts"]["vhwindows"], "%.2f%%" % (self.summary["counts"]["pervhwindows"])), 728 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedoscplikewindows"], self.summary["counts"]["oscplikewindows"], "%.2f%%" % (self.summary["counts"]["peroscplikewindows"])), 729 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedwindows"], self.summary["counts"]["totalwindows"], "%.2f%%" % (self.summary["counts"]["perwindows"])), 730 | ]])) 731 | rows.append("___".join([str(x) for x in [ 732 | "*nix", 733 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedthmnix"], self.summary["counts"]["thmnix"], "%.2f%%" % (self.summary["counts"]["perthmnix"])), 734 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedhtbnix"], self.summary["counts"]["htbnix"], "%.2f%%" % (self.summary["counts"]["perhtbnix"])), 735 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedvhnix"], self.summary["counts"]["vhnix"], "%.2f%%" % (self.summary["counts"]["pervhnix"])), 736 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedoscplikenix"], self.summary["counts"]["oscplikenix"], "%.2f%%" % (self.summary["counts"]["peroscplikenix"])), 737 | "`%s/%s (%s)`" % (self.summary["counts"]["ownednix"], self.summary["counts"]["totalnix"], "%.2f%%" % (self.summary["counts"]["pernix"])), 738 | ]])) 739 | rows.append("___".join([str(x) for x in [ 740 | "OSCPlike", 741 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedthmoscplike"], self.summary["counts"]["thmoscplike"], "%.2f%%" % (self.summary["counts"]["perthmoscplike"])), 742 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedhtboscplike"], self.summary["counts"]["htboscplike"], "%.2f%%" % (self.summary["counts"]["perhtboscplike"])), 743 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedvhoscplike"], self.summary["counts"]["vhoscplike"], "%.2f%%" % (self.summary["counts"]["pervhoscplike"])), 744 | "", 745 | "`%s/%s (%s)`" % (self.summary["counts"]["ownedoscplike"], self.summary["counts"]["totaloscplike"], "%.2f%%" % (self.summary["counts"]["peroscplike"])), 746 | ]])) 747 | return utils.get_table(header, rows, delim="___", markdown=True, colalign="center") 748 | 749 | def stats_owned(self, machines): 750 | header, rows = ["#", "Name", "Infra", "Killchain", "TTPs"], [] 751 | for machine in machines: 752 | if machine["owned_user"] or machine["owned_root"]: 753 | os = utils.to_emoji(machine["os"]) 754 | difficulty = utils.to_emoji(machine["difficulty"]) if machine.get("difficulty") and machine["difficulty"] else utils.to_emoji("difficulty_unknown") 755 | if machine.get("owned_root") and machine["owned_root"]: 756 | owned, owned_tooltip = utils.to_emoji("access_root"), "access_root" 757 | elif machine.get("owned_user") and machine["owned_user"]: 758 | owned, owned_tooltip = utils.to_emoji("access_user"), "access_user" 759 | else: 760 | owned, owned_tooltip = "", "access_none" 761 | if machine["oscplike"]: 762 | oscplike, oscplike_tooltip = utils.to_emoji("oscplike"), "== oscplike" 763 | else: 764 | oscplike, oscplike_tooltip = utils.to_emoji("notoscplike"), "!= oscplike" 765 | name = "[%s](%s#machines)" % (self.config["githubrepourl"], machine["name"]) 766 | infra = "[%s](%s)" % (machine["verbose_id"].replace("hackthebox", "htb").replace("vulnhub", "vh"), machine["url"]) 767 | tags, matrix, rating, writeuppdfurl, killchainurl = [], None, None, None, "" 768 | for writeup in self.summary["readme"]: 769 | if machine["url"] == writeup["url"]: 770 | tags = writeup["tags"] 771 | matrix = '' % (machine["matrixurl"]) if machine["matrixurl"] else None 772 | rating = '' % (machine["ratingsurl"]) if machine["ratingsurl"] else None 773 | writeuppdfurl = machine["writeuppdfurl"] 774 | killchainurl = '' % (machine["killchainurl"]) 775 | if machine["matrixurl"] and machine["ratingsurl"]: 776 | name = "[%s](%s)
%s
%s" % (machine["name"], writeuppdfurl, '' % (machine["matrixurl"]) if machine["matrixurl"] else "", '' % (machine["ratingsurl"]) if machine["ratingsurl"] else "") 777 | elif machine["matrixurl"]: 778 | name = "[%s](%s)
%s" % (machine["name"], writeuppdfurl, '' % (machine["matrixurl"]) if machine["matrixurl"] else "") 779 | elif machine["ratingsurl"]: 780 | name = "[%s](%s)
%s" % (machine["name"], writeuppdfurl, '' % (machine["ratingsurl"]) if machine["ratingsurl"] else "") 781 | else: 782 | name = "[%s](%s)" % (machine["name"], writeuppdfurl) 783 | emptycols = [os, difficulty, owned, oscplike] 784 | os = "" if os == "" else '[`%s`](foo "%s")' % (os, machine["os"]) 785 | difficulty = "" if difficulty == "" else '[`%s`](foo %s)' % (difficulty, '"%spts"' % (machine["points"]) if machine["points"] else "!= pts") 786 | owned = "" if owned == "" else '[`%s`](foo "%s")' % (owned, owned_tooltip) 787 | oscplike = "" if oscplike == "" else '[`%s`](foo "%s")' % (oscplike, oscplike_tooltip) 788 | rows.append("___".join(str(x) for x in [ 789 | name, 790 | infra, 791 | killchainurl, 792 | utils.anchorformat(tags, self.config["githubrepourl"]), 793 | #os, 794 | #difficulty, 795 | #owned, 796 | #oscplike, 797 | ])) 798 | return(utils.get_table(header, ["%d.___%s" % (idx+1, x) for idx, x in enumerate(sorted(rows, key=str.casefold))], delim="___", markdown=True, colalign="center")) 799 | 800 | 801 | if __name__ == "__main__": 802 | parser = argparse.ArgumentParser(description="%s (v%s)" % (utils.blue_bold("svachal"), utils.green_bold("0.1"))) 803 | parser.add_argument('-w', '--writeupdir', required=False, action='store', help='override default writeup dir path') 804 | parser.add_argument('-g', '--githubrepourl', required=False, action='store', help='override default github repo url for writeups') 805 | 806 | sfgroup = parser.add_mutually_exclusive_group() 807 | sfgroup.add_argument('-s', '--start', required=False, action='store', help='initiate new writeup process (provide machine url)') 808 | sfgroup.add_argument('-m', '--manual', required=False, action='store', help='initiate new writeup process (provide infra.name)') 809 | sfgroup.add_argument('-f', '--finish', required=False, action='store_true', help='wrapup writeup process for $PWD writeup directory') 810 | sfgroup.add_argument('-r', '--rebuildall', required=False, action='store_true', help='rebuild all writeups (recreates md/pdf/killchain/matrix)') 811 | sfgroup.add_argument('-z', '--summarize', required=False, action='store_true', help='update summary.yml and readme.md with data from all writeups') 812 | args = parser.parse_args() 813 | 814 | if not args.writeupdir and not args.githubrepourl: 815 | svl = Svachal( 816 | writeupdir="%s/toolbox/projects/writeups" % (utils.expand_env(var="$HOME")), 817 | githubrepourl="https://github.com/7h3rAm/writeups", 818 | ) 819 | elif not args.writeupdir or not args.githubrepourl: 820 | utils.error("must use both writeupdir and githubrepourl to keep links correct") 821 | utils.error("check usage below:") 822 | parser.print_help() 823 | sys.exit(1) 824 | else: 825 | svl = Svachal(writeupdir=args.writeupdir, githubrepourl=args.githubrepourl) 826 | 827 | if args.start: 828 | svl.opcode_start(args.start, manual=False) 829 | 830 | elif args.manual: 831 | svl.opcode_start(args.manual, manual=True) 832 | 833 | elif args.finish: 834 | svl.opcode_finish() 835 | 836 | elif args.rebuildall: 837 | svl.opcode_rebuildall() 838 | 839 | elif args.summarize: 840 | svl.opcode_summarize() 841 | 842 | else: 843 | parser.print_help() 844 | --------------------------------------------------------------------------------