# GitHub Pages - Multiple RCEs via insecure Kramdown configuration - $25,000 Bounty

GitHub Pages Jekyll builds allowed attacker-controlled Kramdown options, enabling path traversal require and arbitrary Ruby class instantiation leading to RCE.

- Payout: $25,000
- Severity: critical
- Program: GitHub
- Credited: William Bowling / vakzz
- Disclosed: 2020-10-20
- CVE: CVE-2020-10518
- Write-up quality: 9/10
- Original: https://devcraft.io/2020/10/20/github-pages-multiple-rces-via-kramdown-config.html
- Library page: https://bugbountylibrary.com/r/blog%3Adevcraft-io-2020-10-20-github-pages-multiple-rces-via-kramdown-config-html
- Tags: domain:web impact:integrity impact:rce kind:bounty platform:blog sev:critical tech:ruby technique:chained technique:source-review vuln:path-traversal vuln:rce

**Target:** Jekyll/Kramdown markdown build pipeline on GitHub Pages
**Root cause:** Kramdown's :input option is passed to require after snake_case, which strips non-alpha chars allowing directory traversal; and formatter_class uses ::Rouge::Formatters.const_get which resolves any top-level constant, letting attacker instantiate arbitrary classes with a controlled options hash.
**Steps:**
1. Set `kramdown: input: ../../../../../../../../tmp/evil.rb` in _config.yml; snake_case strips dots/slashes so require loads /tmp/evil.rb
2. Place payload at a predictable path: push a repo with large files and a code.rb, loop `git push -f` to keep /data/user/tmp/pages/pagebuilds/<user>/<repo> alive
3. Second site sets `kramdown: input: ../../../../../../../../data/user/tmp/pages/pagebuilds/vakzz/jekyll1/code.rb` and builds in a loop until the file is required
4. For formatter path: set `syntax_highlighter_opts: formatter: Hoosegow` and `inmate_dir: /tmp/`; Hoosegow#initialize calls `require File.join(@inmate_dir,'inmate.rb')`
5. Place /tmp/inmate.rb with payload; build site to trigger require and code execution
**How found:** Monitoring GHE release notes for a prior fix revealed CVE-2020-14001 Kramdown template RCE. Testing template option worked locally, then audited Kramdown::Options for other abusable options. Noticed undocumented :input option passed to try_require with snake_case stripping non-alpha chars, hypothesizing traversal. Later revisited formatter_class regex and discovered const_get resolves beyond Rouge namespace, then brute-forced instantiable constants with a hash to find Hoosegow.
**Exploitation:** Attacker with a GitHub Pages repo controls _config.yml Kramdown options. Via traversal in :input they require an attacker-uploaded .rb from a predictable build directory kept alive by large files and rapid force-pushes. Alternatively, formatter_class const_get resolves Hoosegow, whose initializer requires inmate.rb from an attacker-specified inmate_dir, executing arbitrary Ruby on the Pages build server.
**Escalation:** Chained two independent option-injection primitives (require traversal and arbitrary constant instantiation) to achieve RCE despite safe_yaml blocking deserialization.
**Insight:** When a library exposes a 'class name' option validated by regex then resolved via const_get, the lookup is not namespaced—enumerate all top-level constants whose initializer accepts a hash and look for ones that require/load files from hash-controlled paths.
