前言
嗯,这次酣畅淋漓全场爆0一度怀疑Web手是不是睡着了的新生赛我一共出了三道题(有一道还是前一天晚上紧急出的,还因为这个第二天睡过了错过了运动会拔河,淦),最终结果是两道简单题都是33解,中等题4解,其实解出数量有点低于预期,不过后来交流发现造成这么少解的一个很重要的原因是工具很不齐全而且运用也不熟练……或许还是我们赛前工作做少了吧,之后还是可以考虑考虑……
接下来我会按照难度顺序进行Writeup的撰写。
U5er0Agent
打开题目,题目最下面说添加?source=1即可访问题目源码

访问 http://url/?source=1 ,获得源码:
<?php
class RequestProcessor {
private $validation_rules = [
'pattern_blacklist' => [
'primary' => '/cat|flag|env/i',
'secondary' => ['/\.\.\//', '/\/etc\//', '/\/passwd/']
],
'max_length' => 1024,
'allowed_chars' => '/^[a-zA-Z0-9_\-\$\(\)\=\<\>\.\'\"\s\;\\\:\+\*\?\[\]\{\}\|\/]+$/'
];
protected $execution_environment = [];
protected $audit_trail = [];
public function __construct() {
$this->initializeEnvironment();
$this->logEvent('SYSTEM_INIT', 'Request processor initialized');
}
private function initializeEnvironment() {
$this->execution_environment = [
'safe_mode' => false,
'max_execution_time' => 5,
'memory_limit' => '128M'
];
}
public function processUserInput($input_data) {
$validation_result = $this->validateInputString($input_data);
if ($validation_result['status'] === 'VALID') {
$this->logEvent('VALIDATION_PASS', 'Input validation successful');
return $this->executeSecureCode($input_data);
} else {
$this->logEvent('VALIDATION_FAIL', $validation_result['reason']);
return $this->generateErrorResponse($validation_result);
}
}
private function validateInputString($input) {
if (strlen($input) > $this->validation_rules['max_length']) {
return ['status' => 'INVALID', 'reason' => 'Input exceeds maximum length'];
}
if (!preg_match($this->validation_rules['allowed_chars'], $input)) {
return ['status' => 'INVALID', 'reason' => 'Invalid characters detected'];
}
if (preg_match($this->validation_rules['pattern_blacklist']['primary'], $input)) {
return ['status' => 'INVALID', 'reason' => 'Restricted pattern detected'];
}
foreach ($this->validation_rules['pattern_blacklist']['secondary'] as $pattern) {
if (preg_match($pattern, $input)) {
$this->logEvent('SUSPICIOUS_PATTERN', "Secondary pattern matched: $pattern");
}
}
return ['status' => 'VALID', 'reason' => 'All checks passed'];
}
private function executeSecureCode($code_fragment) {
$this->logEvent('EXECUTION_START', 'Beginning secure code execution');
try {
eval($code_fragment);
$this->logEvent('EXECUTION_COMPLETE', 'Code execution finished');
return ['status' => 'SUCCESS', 'message' => 'Execution completed'];
} catch (ParseError $e) {
return ['status' => 'ERROR', 'message' => 'Syntax error in input'];
} catch (Throwable $e) {
return ['status' => 'ERROR', 'message' => 'Runtime error occurred'];
}
}
private function logEvent($event_type, $event_description) {
$log_entry = [
'timestamp' => microtime(true),
'event_type' => $event_type,
'description' => $event_description,
'client_ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
];
array_push($this->audit_trail, $log_entry);
}
private function generateErrorResponse($validation_result) {
$error_templates = [
'Input exceeds maximum length' => 'Payload size violation',
'Invalid characters detected' => 'Character set violation',
'Restricted pattern detected' => 'Content policy violation',
'Syntax error in input' => 'Execution syntax error',
'Runtime error occurred' => 'Execution runtime error'
];
$public_message = $error_templates[$validation_result['reason']] ?? 'Processing error';
return [
'status' => 'ERROR',
'public_message' => $public_message,
'internal_code' => bin2hex(random_bytes(4))
];
}
public function getSystemStatus() {
return [
'version' => '2.1.4',
'environment' => $this->execution_environment,
'audit_entries' => count($this->audit_trail),
'timestamp' => date('c')
];
}
}
$system_processor = new RequestProcessor();
if (isset($_SERVER['HTTP_USER_AGENT'])) {
$user_agent_string = $_SERVER['HTTP_USER_AGENT'];
$processing_result = $system_processor->processUserInput($user_agent_string);
if ($processing_result['status'] !== 'SUCCESS') {
error_log("Processing failed: " . ($processing_result['public_message'] ?? 'Unknown error'));
}
}
echo '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Secure API Gateway</title>
<style>
body {
font-family: "Courier New", monospace;
background: #1a1a1a;
color: #00ff00;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
border: 1px solid #333;
padding: 20px;
background: #0a0a0a;
}
.header {
border-bottom: 1px solid #333;
padding-bottom: 10px;
margin-bottom: 20px;
}
.status-box {
background: #002200;
padding: 10px;
margin: 10px 0;
border-left: 3px solid #00ff00;
}
.footer {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #333;
font-size: 0.8em;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔒 Secure API Gateway</h1>
<p>v2.1.4 | Production Environment</p>
</div>
<div class="status-box">
<strong>System Status:</strong> OPERATIONAL<br>
<strong>Security Level:</strong> HIGH<br>
<strong>Requests Processed:</strong> ' . rand(1000, 9999) . '
</div>
<p>This system processes client requests through the User-Agent header for specialized operations.</p>
<p>All inputs are validated against security policies before execution.</p>
<div class="footer">
<p>© 2024 CTF Security Systems. All rights reserved.</p>
<p>Debug: Add ?source=1 to view source code</p>
</div>
</div>
</body>
</html>';
// Debug source view
if (isset($_GET['source']) && $_GET['source'] == '1') {
highlight_file(__FILE__);
exit;
}
?>源码看着比较复杂,如果看得懂PHP当然可以直接分析,但考虑到接受情况,这里我们就让AI帮忙解读一下:


得知大致代码逻辑是从User Agent里面读取内容,并经过了一个简单的过滤器(主要就过滤了cat,flag,env),然后直接把User Agent里面的内容当作php代码执行。
并且AI也给出了payload:

以下介绍三种payload:
最开始的预期:用字符串拼接(或者通配符)绕过flag黑名单,用tac或者其他读取文件指令绕过cat黑名单


用phpinfo读环境变量里面的flag

用file_get_contents读flag

井字棋小游戏
由于井字棋AI理论上完全下不赢,因此正常赢得游戏是肯定做不出来的。
于是我们打开JS源码:

发现这是一坨啥啊……其实这是混淆后的JS代码,自己去反混淆有点复杂,于是我们让AI帮我们做一下:

发现AI直接把答案报出来了。
于是直接构造exp就行:

POST一个JSON数据,即可得到flag。
Guild
“现在会馆很强大,还轮不到你们这些小家伙来承担责任”
“这次就靠你了”
“接下来……会馆将要顶着最强的名号,迎来最弱的时期”
题目附件给了个jar,我们丢进反编译器:

发现主要操作逻辑的源码都在top.jwmc.kuri.guild包里面(其余是框架内容,分析较为复杂),查看源码:




通过GuildApplication类可以发现用户名是硬编码的admin,密码是一个随机UUID,很显然我们不能直接爆破出来。

于是我们可以通过GuildSecurityConfig类去查看密码验证逻辑:

matches逻辑说明,如果传入的密码计算了hashcode后对65536取模与系统中的密码计算了hashcode后对65536取模相同,那么就认为两个密码相同的。
这里解释一下:这里模拟的是一般服务器的情况,一般服务器当中不会直接存储明文的密码,只会存储经过了某种hash算法之后的结果。而这个题目模拟的是强度不够的hash算法造成的安全问题,从而导致在hash泄露后,哪怕不用原本的密码都能够通过身份认证,从而盗取身份。
然后我们能发现存在一个secret_r0uter,泄露了hash:


于是我们就能利用这点去在本地尝试算出可以用于登录的密码,编写Exp如下:
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.UUID;
public class Exp {
public static void main(String[] args) throws IOException, InterruptedException {
System.out.println("");
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:52937/secret_r0uter"))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("状态码: " + response.statusCode());
System.out.println("响应内容: " + response.body());
int hash = Integer.parseInt(response.body());
String payload = UUID.randomUUID().toString();
while(payload.hashCode()%65536 != hash) {
payload = UUID.randomUUID().toString();
}
System.out.println("Cracked: " + payload);
//登录过程省略
}
}
然后我们就能爆破出password:

然后我们就能利用这个凭据去登录:

登录后打开F12就能看到flag:

结尾碎碎念
这次作为主办方举办这次比赛还是发现了不少问题的,包括不会用AI以及不会用搜索引擎等,以及出现了一点点命题事故,甚至还有直接交别人flag的......但是说实话,目前无论是自己还是整个社团,还是有很长的路要走。组织一次比赛真的挺累的,也非常感谢各位同学的支持!
最后......
Hack for fun not for profit.